diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index 9b07c27526f..7aba51a470b 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -65,7 +65,6 @@ src/vs/code/** @bpasero @deepak1556 src/vs/workbench/services/activity/** @bpasero src/vs/workbench/services/authentication/** @TylerLeonhardt src/vs/workbench/services/auxiliaryWindow/** @bpasero -src/vs/workbench/services/chat/** @bpasero src/vs/workbench/services/contextmenu/** @bpasero src/vs/workbench/services/dialogs/** @alexr00 @bpasero src/vs/workbench/services/editor/** @bpasero @@ -100,15 +99,6 @@ 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/** @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/localization/** @TylerLeonhardt src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @TylerLeonhardt src/vs/workbench/contrib/scm/** @lszomoru diff --git a/.vscode/launch.json b/.vscode/launch.json index 9dbed82ee94..74dfd6a3da6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -602,6 +602,17 @@ "order": 4 } }, + { + "name": "Component Explorer", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5199/___explorer", + "preLaunchTask": "Launch Monaco Editor Vite", + "presentation": { + "group": "monaco", + "order": 4 + } + }, { "name": "Monaco Editor - Self Contained Diff Editor", "type": "chrome", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f601633b570..14e637aaf2d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,10 +1,40 @@ { "version": "2.0.0", "tasks": [ + { + "type": "npm", + "script": "watch-client-transpiled", + "label": "Core - Transpile", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "buildWatchers", + "close": false + }, + "problemMatcher": { + "owner": "esbuild", + "applyTo": "closedDocuments", + "fileLocation": [ + "relative", + "${workspaceFolder}/src" + ], + "pattern": { + "regexp": "^(.+?):(\\d+):(\\d+): ERROR: (.+)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + }, + "background": { + "beginsPattern": "Starting transpilation...", + "endsPattern": "Finished transpilation with" + } + } + }, { "type": "npm", "script": "watch-clientd", - "label": "Core - Build", + "label": "Core - Typecheck", "isBackground": true, "presentation": { "reveal": "never", @@ -60,7 +90,8 @@ { "label": "VS Code - Build", "dependsOn": [ - "Core - Build", + "Core - Transpile", + "Core - Typecheck", "Ext - Build" ], "group": { @@ -69,10 +100,22 @@ }, "problemMatcher": [] }, + { + "type": "npm", + "script": "kill-watch-client-transpiled", + "label": "Kill Core - Transpile", + "group": "build", + "presentation": { + "reveal": "never", + "group": "buildKillers", + "close": true + }, + "problemMatcher": "$tsc" + }, { "type": "npm", "script": "kill-watch-clientd", - "label": "Kill Core - Build", + "label": "Kill Core - Typecheck", "group": "build", "presentation": { "reveal": "never", @@ -96,7 +139,8 @@ { "label": "Kill VS Code - Build", "dependsOn": [ - "Kill Core - Build", + "Kill Core - Transpile", + "Kill Core - Typecheck", "Kill Ext - Build" ], "group": "build", diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 897c27e680d..b440fe34058 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -158,10 +158,6 @@ variables: name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.VSCODE_QUALITY }})" resources: - pipelines: - - pipeline: vscode-7pm-kick-off - source: 'VS Code 7PM Kick-Off' - trigger: true repositories: - repository: 1esPipelines type: git diff --git a/build/buildConfig.ts b/build/buildConfig.ts new file mode 100644 index 00000000000..a4299d2647f --- /dev/null +++ b/build/buildConfig.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. + *--------------------------------------------------------------------------------------------*/ + +/** + * When `true`, self-hosting uses esbuild for fast transpilation (build/next) + * and gulp-tsb only for type-checking (`noEmit`). + * + * When `false`, gulp-tsb does both transpilation and type-checking (old behavior). + */ +export const useEsbuildTranspile = true; diff --git a/build/gulpfile.ts b/build/gulpfile.ts index a57218b8445..1c21618ca60 100644 --- a/build/gulpfile.ts +++ b/build/gulpfile.ts @@ -13,6 +13,7 @@ import { compileExtensionMediaTask, compileExtensionsTask, watchExtensionsTask } import * as compilation from './lib/compilation.ts'; import * as task from './lib/task.ts'; import * as util from './lib/util.ts'; +import { useEsbuildTranspile } from './buildConfig.ts'; const require = createRequire(import.meta.url); @@ -32,7 +33,9 @@ gulp.task(transpileClientTask); const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compilation.compileApiProposalNamesTask, compilation.compileTask('src', 'out', false))); gulp.task(compileClientTask); -const watchClientTask = task.define('watch-client', task.series(util.rimraf('out'), task.parallel(compilation.watchTask('out', false), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask))); +const watchClientTask = useEsbuildTranspile + ? task.define('watch-client', task.parallel(compilation.watchTask('out', false, 'src', { noEmit: true }), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask)) + : task.define('watch-client', task.series(util.rimraf('out'), task.parallel(compilation.watchTask('out', false), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask))); gulp.task(watchClientTask); // All diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 614f2ee7d1c..19504aaf7c7 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -15,7 +15,7 @@ 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 { readISODate, writeISODate } from './lib/date.ts'; import * as task from './lib/task.ts'; import buildfile from './buildfile.ts'; import * as optimize from './lib/optimize.ts'; @@ -30,9 +30,12 @@ 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 { copyCodiconsTask } from './lib/compilation.ts'; +import { useEsbuildTranspile } from './buildConfig.ts'; import { promisify } from 'util'; import globCallback from 'glob'; import rceditCallback from 'rcedit'; +import * as cp from 'child_process'; const glob = promisify(globCallback); @@ -152,6 +155,81 @@ const bundleVSCodeTask = task.define('bundle-vscode', task.series( )); gulp.task(bundleVSCodeTask); +// esbuild-based bundle tasks (drop-in replacement for bundle-vscode / minify-vscode) +function runEsbuildTranspile(outDir: string, excludeTests: boolean): Promise { + return new Promise((resolve, reject) => { + const scriptPath = path.join(root, 'build/next/index.ts'); + const args = [scriptPath, 'transpile', '--out', outDir]; + if (excludeTests) { + args.push('--exclude-tests'); + } + + const proc = cp.spawn(process.execPath, args, { + cwd: root, + stdio: 'inherit' + }); + + proc.on('error', reject); + proc.on('close', code => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`esbuild transpile failed with exit code ${code} (outDir: ${outDir})`)); + } + }); + }); +} + +function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, target: 'desktop' | 'server' | 'server-web' = 'desktop', sourceMapBaseUrl?: string): Promise { + return new Promise((resolve, reject) => { + // const tsxPath = path.join(root, 'build/node_modules/tsx/dist/cli.mjs'); + const scriptPath = path.join(root, 'build/next/index.ts'); + const args = [scriptPath, 'bundle', '--out', outDir, '--target', target]; + if (minify) { + args.push('--minify'); + } + if (nls) { + args.push('--nls'); + } + if (sourceMapBaseUrl) { + args.push('--source-map-base-url', sourceMapBaseUrl); + } + + const proc = cp.spawn(process.execPath, args, { + cwd: root, + stdio: 'inherit' + }); + + proc.on('error', reject); + proc.on('close', code => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`esbuild bundle failed with exit code ${code} (outDir: ${outDir}, minify: ${minify}, nls: ${nls}, target: ${target})`)); + } + }); + }); +} + +function runTsGoTypeCheck(): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn('tsgo', ['--project', 'src/tsconfig.json', '--noEmit', '--skipLibCheck'], { + cwd: root, + stdio: 'inherit', + shell: true + }); + + proc.on('error', reject); + proc.on('close', code => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`tsgo typecheck failed with exit code ${code}`)); + } + }); + }); +} + const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; const minifyVSCodeTask = task.define('minify-vscode', task.series( bundleVSCodeTask, @@ -160,7 +238,7 @@ const minifyVSCodeTask = task.define('minify-vscode', task.series( )); gulp.task(minifyVSCodeTask); -const coreCI = task.define('core-ci', task.series( +const coreCIOld = task.define('core-ci-old', task.series( gulp.task('compile-build-with-mangling') as task.Task, task.parallel( gulp.task('minify-vscode') as task.Task, @@ -168,7 +246,28 @@ const coreCI = task.define('core-ci', task.series( gulp.task('minify-vscode-reh-web') as task.Task, ) )); -gulp.task(coreCI); +gulp.task(coreCIOld); + +const coreCIEsbuild = task.define('core-ci-esbuild', task.series( + copyCodiconsTask, + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, + compileExtensionMediaBuildTask, + writeISODate('out-build'), + // Type-check with tsgo (no emit) + task.define('tsgo-typecheck', () => runTsGoTypeCheck()), + // Transpile individual files to out-build first (for unit tests) + task.define('esbuild-out-build', () => runEsbuildTranspile('out-build', false)), + // Then bundle for shipping (bundles also write NLS files to out-build) + task.parallel( + task.define('esbuild-vscode-min', () => runEsbuildBundle('out-vscode-min', true, true, 'desktop', `${sourceMappingURLBase}/core`)), + task.define('esbuild-vscode-reh-min', () => runEsbuildBundle('out-vscode-reh-min', true, true, 'server', `${sourceMappingURLBase}/core`)), + task.define('esbuild-vscode-reh-web-min', () => runEsbuildBundle('out-vscode-reh-web-min', true, true, 'server-web', `${sourceMappingURLBase}/core`)), + ) +)); +gulp.task(coreCIEsbuild); + +gulp.task(task.define('core-ci', useEsbuildTranspile ? coreCIEsbuild : coreCIOld)); const coreCIPR = task.define('core-ci-pr', task.series( gulp.task('compile-build-without-mangling') as task.Task, @@ -516,27 +615,44 @@ BUILD_TARGETS.forEach(buildTarget => { const sourceFolderName = `out-vscode${dashed(minified)}`; const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`; - const tasks = [ + const packageTasks: task.Task[] = [ compileNativeExtensionsBuildTask, util.rimraf(path.join(buildRoot, destinationFolderName)), packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) ]; if (platform === 'win32') { - tasks.push(patchWin32DependenciesTask(destinationFolderName)); + packageTasks.push(patchWin32DependenciesTask(destinationFolderName)); } - const vscodeTaskCI = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(...tasks)); + const vscodeTaskCI = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(...packageTasks)); 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 - )); + let vscodeTask: task.Task; + if (useEsbuildTranspile) { + const esbuildBundleTask = task.define( + `esbuild-bundle${dashed(platform)}${dashed(arch)}${dashed(minified)}`, + () => runEsbuildBundle(sourceFolderName, !!minified, true, 'desktop', minified ? `${sourceMappingURLBase}/core` : undefined) + ); + vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( + copyCodiconsTask, + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, + compileExtensionMediaBuildTask, + writeISODate('out-build'), + esbuildBundleTask, + vscodeTaskCI + )); + } else { + 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; @@ -568,7 +684,7 @@ const innoSetupConfig: Record { + return new Promise((resolve, reject) => { + const scriptPath = path.join(REPO_ROOT, 'build/next/index.ts'); + const args = [scriptPath, 'bundle', '--out', outDir, '--target', 'web']; + if (minify) { + args.push('--minify'); + } + if (nls) { + args.push('--nls'); + } + + const proc = cp.spawn(process.execPath, args, { + cwd: REPO_ROOT, + stdio: 'inherit' + }); + + proc.on('error', reject); + proc.on('close', code => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`esbuild web bundle failed with exit code ${code} (outDir: ${outDir}, minify: ${minify}, nls: ${nls})`)); + } + }); + }); +} + export const vscodeWebResourceIncludes = [ // NLS @@ -110,7 +140,7 @@ export const createVSCodeWebFileContentMapper = (extensionsRoot: string, product }; }; -const bundleVSCodeWebTask = task.define('bundle-vscode-web', task.series( +const bundleVSCodeWebTask = task.define('bundle-vscode-web-OLD', task.series( util.rimraf('out-vscode-web'), optimize.bundleTask( { @@ -125,13 +155,17 @@ const bundleVSCodeWebTask = task.define('bundle-vscode-web', task.series( ) )); -const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( +const minifyVSCodeWebTask = task.define('minify-vscode-web-OLD', 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); +// esbuild-based tasks (new) +const esbuildBundleVSCodeWebTask = task.define('esbuild-vscode-web', () => runEsbuildBundle('out-vscode-web', false, true)); +const esbuildBundleVSCodeWebMinTask = task.define('esbuild-vscode-web-min', () => runEsbuildBundle('out-vscode-web-min', true, true)); + function packageTask(sourceFolderName: string, destinationFolderName: string) { const destination = path.join(BUILD_ROOT, destinationFolderName); @@ -197,8 +231,9 @@ const dashed = (str: string) => (str ? `-${str}` : ``); const destinationFolderName = `vscode-web`; const vscodeWebTaskCI = task.define(`vscode-web${dashed(minified)}-ci`, task.series( + copyCodiconsTask, compileWebExtensionsBuildTask, - minified ? minifyVSCodeWebTask : bundleVSCodeWebTask, + minified ? esbuildBundleVSCodeWebMinTask : esbuildBundleVSCodeWebTask, util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), packageTask(sourceFolderName, destinationFolderName) )); diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index f440dc28dd0..2484e884a55 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -49,14 +49,18 @@ interface ICompileTaskOptions { readonly emitError: boolean; readonly transpileOnly: boolean | { esbuild: boolean }; readonly preserveEnglish: boolean; + readonly noEmit?: boolean; } -export function createCompile(src: string, { build, emitError, transpileOnly, preserveEnglish }: ICompileTaskOptions) { +export function createCompile(src: string, { build, emitError, transpileOnly, preserveEnglish, noEmit }: ICompileTaskOptions) { const projectPath = path.join(import.meta.dirname, '../../', src, 'tsconfig.json'); const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; if (!build) { overrideOptions.inlineSourceMap = true; } + if (noEmit) { + overrideOptions.noEmit = true; + } const compilation = tsb.create(projectPath, overrideOptions, { verbose: false, @@ -163,10 +167,10 @@ export function compileTask(src: string, out: string, build: boolean, options: { return task; } -export function watchTask(out: string, build: boolean, srcPath: string = 'src'): task.StreamTask { +export function watchTask(out: string, build: boolean, srcPath: string = 'src', options?: { noEmit?: boolean }): task.StreamTask { const task = () => { - const compile = createCompile(srcPath, { build, emitError: false, transpileOnly: false, preserveEnglish: false }); + const compile = createCompile(srcPath, { build, emitError: false, transpileOnly: false, preserveEnglish: false, noEmit: options?.noEmit }); const src = gulp.src(`${srcPath}/**`, { base: srcPath }); const watchSrc = watch(`${srcPath}/**`, { base: srcPath, readDelay: 200 }); diff --git a/build/lib/nls-analysis.ts b/build/lib/nls-analysis.ts new file mode 100644 index 00000000000..2b00da68c7a --- /dev/null +++ b/build/lib/nls-analysis.ts @@ -0,0 +1,317 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from 'typescript'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface ISpan { + start: ts.LineAndCharacter; + end: ts.LineAndCharacter; +} + +export interface ILocalizeCall { + keySpan: ISpan; + key: string; + valueSpan: ISpan; + value: string; +} + +// ============================================================================ +// AST Collection +// ============================================================================ + +export const CollectStepResult = Object.freeze({ + Yes: 'Yes', + YesAndRecurse: 'YesAndRecurse', + No: 'No', + NoAndRecurse: 'NoAndRecurse' +}); + +export type CollectStepResult = typeof CollectStepResult[keyof typeof CollectStepResult]; + +export function collect(node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] { + const result: ts.Node[] = []; + + function loop(node: ts.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; +} + +export function isImportNode(node: ts.Node): boolean { + return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; +} + +export function isCallExpressionWithinTextSpanCollectStep(textSpan: ts.TextSpan, node: ts.Node): CollectStepResult { + 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; +} + +// ============================================================================ +// Language Service Host +// ============================================================================ + +export class SingleFileServiceHost implements ts.LanguageServiceHost { + private file: ts.IScriptSnapshot; + private lib: ts.IScriptSnapshot; + private options: ts.CompilerOptions; + private filename: string; + + constructor(options: ts.CompilerOptions, filename: string, contents: string) { + 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: string) => name === this.filename ? this.file : this.lib; + getCurrentDirectory = () => ''; + getDefaultLibFileName = () => 'lib.d.ts'; + + readFile(path: string): string | undefined { + if (path === this.filename) { + return this.file.getText(0, this.file.getLength()); + } + return undefined; + } + + fileExists(path: string): boolean { + return path === this.filename; + } +} + +// ============================================================================ +// Analysis +// ============================================================================ + +/** + * Analyzes TypeScript source code to find localize() or localize2() calls. + */ +export function analyzeLocalizeCalls( + contents: string, + functionName: 'localize' | 'localize2' +): ILocalizeCall[] { + const filename = 'file.ts'; + const options: ts.CompilerOptions = { noResolve: true }; + const serviceHost = new SingleFileServiceHost(options, filename, contents); + const service = ts.createLanguageService(serviceHost); + const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); + + // Find all imports + const imports = collect(sourceFile, n => isImportNode(n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse); + + // import nls = require('vs/nls'); + const importEqualsDeclarations = imports + .filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration) + .map(n => n as ts.ImportEqualsDeclaration) + .filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) + .filter(d => { + const text = (d.moduleReference as ts.ExternalModuleReference).expression.getText(); + return text.endsWith(`/nls'`) || text.endsWith(`/nls"`) || text.endsWith(`/nls.js'`) || text.endsWith(`/nls.js"`); + }); + + // import ... from 'vs/nls'; + const importDeclarations = imports + .filter(n => n.kind === ts.SyntaxKind.ImportDeclaration) + .map(n => n as ts.ImportDeclaration) + .filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) + .filter(d => { + const text = d.moduleSpecifier.getText(); + return text.endsWith(`/nls'`) || text.endsWith(`/nls"`) || text.endsWith(`/nls.js'`) || text.endsWith(`/nls.js"`); + }) + .filter(d => !!d.importClause && !!d.importClause.namedBindings); + + // `nls.localize(...)` calls via namespace import + const nlsLocalizeCallExpressions: ts.CallExpression[] = []; + + const namespaceImports = importDeclarations + .filter(d => d.importClause?.namedBindings?.kind === ts.SyntaxKind.NamespaceImport) + .map(d => (d.importClause!.namedBindings as ts.NamespaceImport).name); + + const importEqualsNames = importEqualsDeclarations.map(d => d.name); + + for (const name of [...namespaceImports, ...importEqualsNames]) { + const refs = service.getReferencesAtPosition(filename, name.pos + 1) ?? []; + for (const ref of refs) { + if (ref.isWriteAccess) { + continue; + } + const calls = collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ref.textSpan, n)); + const lastCall = calls[calls.length - 1] as ts.CallExpression | undefined; + if (lastCall && + lastCall.expression.kind === ts.SyntaxKind.PropertyAccessExpression && + (lastCall.expression as ts.PropertyAccessExpression).name.getText() === functionName) { + nlsLocalizeCallExpressions.push(lastCall); + } + } + } + + // `localize` named imports + const namedImports = importDeclarations + .filter(d => d.importClause?.namedBindings?.kind === ts.SyntaxKind.NamedImports) + .flatMap(d => Array.from((d.importClause!.namedBindings! as ts.NamedImports).elements)); + + const localizeCallExpressions: ts.CallExpression[] = []; + + // Direct named import: import { localize } from 'vs/nls' + for (const namedImport of namedImports) { + const isTarget = namedImport.name.getText() === functionName || + (namedImport.propertyName && namedImport.propertyName.getText() === functionName); + + if (!isTarget) { + continue; + } + + const searchName = namedImport.propertyName ? namedImport.name : namedImport.name; + const refs = service.getReferencesAtPosition(filename, searchName.pos + 1) ?? []; + + for (const ref of refs) { + if (ref.isWriteAccess) { + continue; + } + const calls = collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ref.textSpan, n)); + const lastCall = calls[calls.length - 1] as ts.CallExpression | undefined; + if (lastCall) { + localizeCallExpressions.push(lastCall); + } + } + } + + // Combine and deduplicate + const allCalls = [...nlsLocalizeCallExpressions, ...localizeCallExpressions]; + const seen = new Set(); + const uniqueCalls = allCalls.filter(call => { + const start = call.getStart(); + if (seen.has(start)) { + return false; + } + seen.add(start); + return true; + }); + + // Convert to ILocalizeCall + return uniqueCalls + .filter(e => e.arguments.length > 1) + .sort((a, b) => a.arguments[0].getStart() - b.arguments[0].getStart()) + .map(e => { + const args = e.arguments; + return { + keySpan: { + start: ts.getLineAndCharacterOfPosition(sourceFile, args[0].getStart()), + end: ts.getLineAndCharacterOfPosition(sourceFile, args[0].getEnd()) + }, + key: args[0].getText(), + valueSpan: { + start: ts.getLineAndCharacterOfPosition(sourceFile, args[1].getStart()), + end: ts.getLineAndCharacterOfPosition(sourceFile, args[1].getEnd()) + }, + value: args[1].getText() + }; + }); +} + +// ============================================================================ +// Text Model for patching +// ============================================================================ + +export class TextModel { + private lines: string[]; + private lineEndings: string[]; + + constructor(contents: string) { + const regex = /\r\n|\r|\n/g; + let index = 0; + let match: RegExpExecArray | null; + + 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: number): string { + return this.lines[index]; + } + + set(index: number, line: string): void { + this.lines[index] = line; + } + + get lineCount(): number { + return this.lines.length; + } + + /** + * Applies patch(es) to the model. + * Multiple patches must be ordered. + * Does not support patches spanning multiple lines. + */ + apply(span: ISpan, content: string): void { + const startLineNumber = span.start.line; + const endLineNumber = span.end.line; + + const startLine = this.lines[startLineNumber] || ''; + const endLine = this.lines[endLineNumber] || ''; + + this.lines[startLineNumber] = [ + startLine.substring(0, span.start.character), + content, + endLine.substring(span.end.character) + ].join(''); + + for (let i = startLineNumber + 1; i <= endLineNumber; i++) { + this.lines[i] = ''; + } + } + + toString(): string { + let result = ''; + for (let i = 0; i < this.lines.length; i++) { + result += this.lines[i] + this.lineEndings[i]; + } + return result; + } +} + +// ============================================================================ +// Utilities +// ============================================================================ + +/** + * Parses a localize key or value expression. + * sourceExpression can be "foo", 'foo', `foo` or { key: 'foo', comment: [...] } + */ +export function parseLocalizeKeyOrValue(sourceExpression: string): string | { key: string; comment?: string[] } { + // eslint-disable-next-line no-eval + return eval(`(${sourceExpression})`); +} diff --git a/build/lib/nls.ts b/build/lib/nls.ts index 39cc07d9d01..1c054c97399 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -10,45 +10,10 @@ import File from 'vinyl'; import sm from 'source-map'; import path from 'path'; import sort from 'gulp-sort'; +import { type ISpan, analyzeLocalizeCalls, TextModel, parseLocalizeKeyOrValue } from './nls-analysis.ts'; type FileWithSourcemap = File & { sourceMap: sm.RawSourceMap }; -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[] = []; - - function loop(node: ts.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: T): T { - const result: Record = {}; - for (const id in object) { - result[id] = object[id]; - } - return result as T; -} - /** * Returns a stream containing the patched JavaScript and source maps. */ @@ -117,10 +82,6 @@ globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`), 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; -} - const _nls = (() => { const moduleToNLSKeys: { [name: string /* module ID */]: ILocalizeKey[] /* keys */ } = {}; @@ -138,22 +99,6 @@ const _nls = (() => { nlsKeys?: ILocalizeKey[]; } - interface ISpan { - start: ts.LineAndCharacter; - end: ts.LineAndCharacter; - } - - interface ILocalizeCall { - keySpan: ISpan; - key: string; - valueSpan: ISpan; - value: string; - } - - interface ILocalizeAnalysisResult { - localizeCalls: ILocalizeCall[]; - } - interface IPatch { span: ISpan; content: string; @@ -176,212 +121,11 @@ const _nls = (() => { return { line: position.line - 1, character: position.column }; } - class SingleFileServiceHost implements ts.LanguageServiceHost { - - private file: ts.IScriptSnapshot; - private lib: ts.IScriptSnapshot; - private options: ts.CompilerOptions; - private filename: 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(''); - } - - getCompilationSettings = () => this.options; - getScriptFileNames = () => [this.filename]; - getScriptVersion = () => '1'; - getScriptSnapshot = (name: string) => name === this.filename ? this.file : this.lib; - getCurrentDirectory = () => ''; - getDefaultLibFileName = () => 'lib.d.ts'; - - readFile(path: string, _encoding?: string): string | undefined { - if (path === this.filename) { - return this.file.getText(0, this.file.getLength()); - } - return undefined; - } - fileExists(path: string): boolean { - return path === this.filename; - } - } - - function isCallExpressionWithinTextSpanCollectStep(ts: typeof import('typescript'), textSpan: ts.TextSpan, node: ts.Node): CollectStepResult { - 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: typeof import('typescript'), - contents: string, - functionName: 'localize' | 'localize2', - options: ts.CompilerOptions = {} - ): ILocalizeAnalysisResult { - 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 = lazy(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 as ts.ImportEqualsDeclaration) - .filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) - .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 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); - - // `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 as ts.NamespaceImport).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 => lazy(a).last()) - .filter(n => !!n) - .map(n => n as ts.CallExpression) - - // only `localize` calls - .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 => (d.importClause!.namedBindings! as ts.NamedImports).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 => lazy(a).last()) - .filter(n => !!n) - .map(n => n as ts.CallExpression); - - // 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 { - - private lines: string[]; - private lineEndings: string[]; - - constructor(contents: string) { - const regex = /\r\n|\r|\n/g; - let index = 0; - let match: RegExpExecArray | null; - - 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(''); - } - } - - public get(index: number): string { - return this.lines[index]; - } - - public set(index: number, line: string): void { - this.lines[index] = line; - } - - public get lineCount(): number { - return this.lines.length; - } - - /** - * Applies patch(es) to the model. - * Multiple patches must be ordered. - * Does not support patches spanning multiple lines. - */ - public apply(patch: IPatch): void { - 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] = ''; - } - } - - public toString(): string { - return lazy(this.lines).zip(this.lineEndings) - .flatten().toArray().join(''); - } - } - function patchJavascript(patches: IPatch[], contents: string): string { const model = new TextModel(contents); // patch the localize calls - lazy(patches).reverse().each(p => model.apply(p)); + lazy(patches).reverse().each(p => model.apply(p.span, p.content)); return model.toString(); } @@ -431,24 +175,16 @@ const _nls = (() => { return JSON.parse(smg.toString()); } - function parseLocalizeKeyOrValue(sourceExpression: string) { - // 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: typeof import('typescript'), typescript: string, javascript: string, sourcemap: sm.RawSourceMap, options: { preserveEnglish: boolean }): INlsPatchResult { - const { localizeCalls } = analyze(ts, typescript, 'localize'); - const { localizeCalls: localize2Calls } = analyze(ts, typescript, 'localize2'); + function patch(typescript: string, javascript: string, sourcemap: sm.RawSourceMap, options: { preserveEnglish: boolean }): INlsPatchResult { + const localizeCalls = analyzeLocalizeCalls(typescript, 'localize'); + const localize2Calls = analyzeLocalizeCalls(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 nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value) as string).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value) as string)); const smc = new sm.SourceMapConsumer(sourcemap); const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); @@ -505,7 +241,6 @@ const _nls = (() => { .replace(/\\/g, '/'); const { javascript, sourcemap, nlsKeys, nlsMessages } = patch( - ts, typescript, javascriptFile.contents!.toString(), javascriptFile.sourceMap, diff --git a/build/lib/watch/watch-win32.ts b/build/lib/watch/watch-win32.ts index 12b8ffc0ac3..5b5a1197762 100644 --- a/build/lib/watch/watch-win32.ts +++ b/build/lib/watch/watch-win32.ts @@ -10,8 +10,9 @@ import File from 'vinyl'; import es from 'event-stream'; import filter from 'gulp-filter'; import { Stream } from 'stream'; +import { fileURLToPath } from 'url'; -const watcherPath = path.join(import.meta.dirname, 'watcher.exe'); +const watcherPath = path.join(typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)), 'watcher.exe'); function toChangeType(type: '0' | '1' | '2'): 'change' | 'add' | 'unlink' { switch (type) { diff --git a/build/next/index.ts b/build/next/index.ts new file mode 100644 index 00000000000..8d8429012de --- /dev/null +++ b/build/next/index.ts @@ -0,0 +1,1134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as esbuild from 'esbuild'; +import * as fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; +import glob from 'glob'; +import gulpWatch from '../lib/watch/index.ts'; +import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from './nls-plugin.ts'; +import { getVersion } from '../lib/getVersion.ts'; +import product from '../../product.json' with { type: 'json' }; +import packageJson from '../../package.json' with { type: 'json' }; +import { useEsbuildTranspile } from '../buildConfig.ts'; + +const globAsync = promisify(glob); + +// ============================================================================ +// Configuration +// ============================================================================ + +const REPO_ROOT = path.dirname(path.dirname(import.meta.dirname)); +const commit = getVersion(REPO_ROOT); +const quality = (product as { quality?: string }).quality; +const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; + +// CLI: transpile [--watch] | bundle [--minify] [--nls] [--out ] +const command = process.argv[2]; // 'transpile' or 'bundle' + +function getArgValue(name: string): string | undefined { + const index = process.argv.indexOf(name); + if (index !== -1 && index + 1 < process.argv.length) { + return process.argv[index + 1]; + } + return undefined; +} + +const options = { + watch: process.argv.includes('--watch'), + minify: process.argv.includes('--minify'), + nls: process.argv.includes('--nls'), + excludeTests: process.argv.includes('--exclude-tests'), + out: getArgValue('--out'), + target: getArgValue('--target') ?? 'desktop', // 'desktop' | 'server' | 'server-web' | 'web' + sourceMapBaseUrl: getArgValue('--source-map-base-url'), +}; + +// Build targets +type BuildTarget = 'desktop' | 'server' | 'server-web' | 'web'; + +const SRC_DIR = 'src'; +const OUT_DIR = 'out'; +const OUT_VSCODE_DIR = 'out-vscode'; + +// UTF-8 BOM - added to test files with 'utf8' in the path (matches gulp build behavior) +const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]); + +// ============================================================================ +// Entry Points (from build/buildfile.ts) +// ============================================================================ + +// Workers - shared between targets +const workerEntryPoints = [ + 'vs/editor/common/services/editorWebWorkerMain', + 'vs/workbench/api/worker/extensionHostWorkerMain', + 'vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain', + 'vs/workbench/services/languageDetection/browser/languageDetectionWebWorkerMain', + 'vs/workbench/services/search/worker/localFileSearchMain', + 'vs/workbench/contrib/output/common/outputLinkComputerMain', + 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain', +]; + +// Desktop-only workers (use electron-browser) +const desktopWorkerEntryPoints = [ + 'vs/platform/profiling/electron-browser/profileAnalysisWorkerMain', +]; + +// Desktop workbench and code entry points +const desktopEntryPoints = [ + 'vs/workbench/workbench.desktop.main', + 'vs/workbench/contrib/debug/node/telemetryApp', + 'vs/platform/files/node/watcher/watcherMain', + 'vs/platform/terminal/node/ptyHostMain', + 'vs/workbench/api/node/extensionHostProcess', +]; + +const codeEntryPoints = [ + 'vs/code/node/cliProcessMain', + 'vs/code/electron-utility/sharedProcess/sharedProcessMain', + 'vs/code/electron-browser/workbench/workbench', +]; + +// Web entry points (used in server-web and vscode-web) +const webEntryPoints = [ + 'vs/workbench/workbench.web.main.internal', + 'vs/code/browser/workbench/workbench', +]; + +const keyboardMapEntryPoints = [ + 'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux', + 'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.darwin', + 'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win', +]; + +// Server entry points (reh) +const serverEntryPoints = [ + 'vs/workbench/api/node/extensionHostProcess', + 'vs/platform/files/node/watcher/watcherMain', + 'vs/platform/terminal/node/ptyHostMain', +]; + +// Bootstrap files per target +const bootstrapEntryPointsDesktop = [ + 'main', + 'cli', + 'bootstrap-fork', +]; + +const bootstrapEntryPointsServer = [ + 'server-main', + 'server-cli', + 'bootstrap-fork', +]; + +/** + * Get entry points for a build target. + */ +function getEntryPointsForTarget(target: BuildTarget): string[] { + switch (target) { + case 'desktop': + return [ + ...workerEntryPoints, + ...desktopWorkerEntryPoints, + ...desktopEntryPoints, + ...codeEntryPoints, + ]; + case 'server': + return [ + ...serverEntryPoints, + ]; + case 'server-web': + return [ + ...serverEntryPoints, + ...workerEntryPoints, + ...webEntryPoints, + ...keyboardMapEntryPoints, + ]; + case 'web': + return [ + ...workerEntryPoints, + 'vs/workbench/workbench.web.main.internal', // web workbench only (no browser shell) + ...keyboardMapEntryPoints, + ]; + default: + throw new Error(`Unknown target: ${target}`); + } +} + +/** + * Get bootstrap entry points for a build target. + */ +function getBootstrapEntryPointsForTarget(target: BuildTarget): string[] { + switch (target) { + case 'desktop': + return bootstrapEntryPointsDesktop; + case 'server': + case 'server-web': + return bootstrapEntryPointsServer; + case 'web': + return []; // Web has no bootstrap files (served by external server) + default: + throw new Error(`Unknown target: ${target}`); + } +} + +/** + * Get entry points that should bundle CSS (workbench mains). + */ +function getCssBundleEntryPointsForTarget(target: BuildTarget): Set { + switch (target) { + case 'desktop': + return new Set([ + 'vs/workbench/workbench.desktop.main', + 'vs/code/electron-browser/workbench/workbench', + ]); + case 'server': + return new Set(); // Server has no UI + case 'server-web': + return new Set([ + 'vs/workbench/workbench.web.main.internal', + 'vs/code/browser/workbench/workbench', + ]); + case 'web': + return new Set([ + 'vs/workbench/workbench.web.main.internal', + ]); + default: + throw new Error(`Unknown target: ${target}`); + } +} + +// ============================================================================ +// Resource Patterns (files to copy, not transpile/bundle) +// ============================================================================ + +// Common resources needed by all targets +const commonResourcePatterns = [ + // Tree-sitter queries + 'vs/editor/common/languages/highlights/*.scm', + 'vs/editor/common/languages/injections/*.scm', +]; + +// Resources only needed for dev/transpile builds (these get bundled into the main +// JS/CSS bundles for production, so separate copies are redundant) +const devOnlyResourcePatterns = [ + // Fonts (esbuild file loader copies to media/codicon.ttf for production) + 'vs/base/browser/ui/codicons/codicon/codicon.ttf', + + // Vendor JavaScript libraries (bundled into workbench main JS for production) + 'vs/base/common/marked/marked.js', + 'vs/base/common/semver/semver.js', + 'vs/base/browser/dompurify/dompurify.js', +]; + +// Resources for desktop target +const desktopResourcePatterns = [ + ...commonResourcePatterns, + + // HTML + 'vs/code/electron-browser/workbench/workbench.html', + 'vs/code/electron-browser/workbench/workbench-dev.html', + 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', + 'vs/workbench/contrib/webview/browser/pre/*.html', + + // Webview pre scripts + 'vs/workbench/contrib/webview/browser/pre/*.js', + + // Shell scripts + 'vs/base/node/*.sh', + 'vs/workbench/contrib/terminal/common/scripts/*.sh', + 'vs/workbench/contrib/terminal/common/scripts/*.ps1', + 'vs/workbench/contrib/terminal/common/scripts/*.psm1', + 'vs/workbench/contrib/terminal/common/scripts/*.fish', + 'vs/workbench/contrib/terminal/common/scripts/*.zsh', + 'vs/workbench/contrib/externalTerminal/**/*.scpt', + + // Media - audio + 'vs/platform/accessibilitySignal/browser/media/*.mp3', + + // Media - images + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg', + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png', + 'vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', + 'vs/workbench/services/extensionManagement/common/media/*.svg', + 'vs/workbench/services/extensionManagement/common/media/*.png', + 'vs/workbench/browser/parts/editor/media/*.png', + 'vs/workbench/contrib/debug/browser/media/*.png', +]; + +// Resources for server target (minimal - no UI) +const serverResourcePatterns = [ + // Shell scripts for process monitoring + 'vs/base/node/cpuUsage.sh', + 'vs/base/node/ps.sh', + + // External Terminal + 'vs/workbench/contrib/externalTerminal/**/*.scpt', + + // Terminal shell integration + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1', + 'vs/workbench/contrib/terminal/common/scripts/CodeTabExpansion.psm1', + 'vs/workbench/contrib/terminal/common/scripts/GitTabExpansion.psm1', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-env.zsh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish', +]; + +// Resources for server-web target (server + web UI) +const serverWebResourcePatterns = [ + ...serverResourcePatterns, + ...commonResourcePatterns, + + // Web HTML + 'vs/code/browser/workbench/workbench.html', + 'vs/code/browser/workbench/workbench-dev.html', + 'vs/code/browser/workbench/callback.html', + 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', + 'vs/workbench/contrib/webview/browser/pre/*.html', + + // Webview pre scripts + 'vs/workbench/contrib/webview/browser/pre/*.js', + + // Media - audio + 'vs/platform/accessibilitySignal/browser/media/*.mp3', + + // Media - images + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg', + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png', + 'vs/workbench/contrib/extensions/browser/media/*.svg', + 'vs/workbench/contrib/extensions/browser/media/*.png', + 'vs/workbench/services/extensionManagement/common/media/*.svg', + 'vs/workbench/services/extensionManagement/common/media/*.png', +]; + +// Resources for standalone web target (browser-only, no server) +const webResourcePatterns = [ + ...commonResourcePatterns, + + // Web HTML + 'vs/code/browser/workbench/workbench.html', + 'vs/code/browser/workbench/workbench-dev.html', + 'vs/code/browser/workbench/callback.html', + 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', + 'vs/workbench/contrib/webview/browser/pre/*.html', + + // Webview pre scripts + 'vs/workbench/contrib/webview/browser/pre/*.js', + + // Media - audio + 'vs/platform/accessibilitySignal/browser/media/*.mp3', + + // Media - images + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg', + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png', + 'vs/workbench/contrib/extensions/browser/media/*.svg', + 'vs/workbench/contrib/extensions/browser/media/*.png', + 'vs/workbench/services/extensionManagement/common/media/*.svg', + 'vs/workbench/services/extensionManagement/common/media/*.png', +]; + +/** + * Get resource patterns for a build target. + */ +function getResourcePatternsForTarget(target: BuildTarget): string[] { + switch (target) { + case 'desktop': + return desktopResourcePatterns; + case 'server': + return serverResourcePatterns; + case 'server-web': + return serverWebResourcePatterns; + case 'web': + return webResourcePatterns; + default: + throw new Error(`Unknown target: ${target}`); + } +} + +// Test fixtures (only copied for development builds, not production) +const testFixturePatterns = [ + '**/test/**/*.json', + '**/test/**/*.txt', + '**/test/**/*.snap', + '**/test/**/*.tst', + '**/test/**/*.html', + '**/test/**/*.js', + '**/test/**/*.jxs', + '**/test/**/*.tsx', + '**/test/**/*.css', + '**/test/**/*.png', + '**/test/**/*.md', + '**/test/**/*.zip', + '**/test/**/*.pdf', + '**/test/**/*.qwoff', + '**/test/**/*.wuff', + '**/test/**/*.less', + // Files without extensions (executables, etc.) + '**/test/**/fixtures/executable/*', +]; + +// ============================================================================ +// Utilities +// ============================================================================ + +async function cleanDir(dir: string): Promise { + const fullPath = path.join(REPO_ROOT, dir); + console.log(`[clean] ${dir}`); + await fs.promises.rm(fullPath, { recursive: true, force: true }); + await fs.promises.mkdir(fullPath, { recursive: true }); +} + +/** + * Scan for built-in extensions in the given directory. + * Returns an array of extension entries for the builtinExtensionsScannerService. + */ +function scanBuiltinExtensions(extensionsRoot: string): Array<{ extensionPath: string; packageJSON: unknown }> { + const result: Array<{ extensionPath: string; packageJSON: unknown }> = []; + const extensionsPath = path.join(REPO_ROOT, extensionsRoot); + + if (!fs.existsSync(extensionsPath)) { + return result; + } + + for (const entry of fs.readdirSync(extensionsPath, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + const packageJsonPath = path.join(extensionsPath, entry.name, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + try { + const packageJSON = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + result.push({ + extensionPath: entry.name, + packageJSON + }); + } catch (e) { + // Skip invalid extensions + } + } + } + + return result; +} + +/** + * Get the date from the out directory date file, or return current date. + */ +function readISODate(outDir: string): string { + try { + return fs.readFileSync(path.join(REPO_ROOT, outDir, 'date'), 'utf8'); + } catch { + return new Date().toISOString(); + } +} + +/** + * Only used to make encoding tests happy. The source files don't have a BOM but the + * tests expect one... so we add it here. + */ +function needsBomAdded(filePath: string): boolean { + return /([\/\\])test\1.*utf8/.test(filePath); +} + +async function copyFile(srcPath: string, destPath: string): Promise { + await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); + + if (needsBomAdded(srcPath)) { + const content = await fs.promises.readFile(srcPath); + if (content[0] !== 0xef || content[1] !== 0xbb || content[2] !== 0xbf) { + await fs.promises.writeFile(destPath, Buffer.concat([UTF8_BOM, content])); + return; + } + } + await fs.promises.copyFile(srcPath, destPath); +} + +/** + * Standalone TypeScript files that need to be compiled separately (not bundled). + * These run in special contexts (e.g., Electron preload) where bundling isn't appropriate. + * Only needed for desktop target. + */ +const desktopStandaloneFiles = [ + 'vs/base/parts/sandbox/electron-browser/preload.ts', + 'vs/base/parts/sandbox/electron-browser/preload-aux.ts', + 'vs/platform/browserView/electron-browser/preload-browserView.ts', +]; + +async function compileStandaloneFiles(outDir: string, doMinify: boolean, target: BuildTarget): Promise { + // Only desktop needs preload scripts + if (target !== 'desktop') { + return; + } + + console.log(`[standalone] Compiling ${desktopStandaloneFiles.length} standalone files...`); + + const banner = `/*!-------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/`; + + await Promise.all(desktopStandaloneFiles.map(async (file) => { + const entryPath = path.join(REPO_ROOT, SRC_DIR, file); + const outPath = path.join(REPO_ROOT, outDir, file.replace(/\.ts$/, '.js')); + + await esbuild.build({ + entryPoints: [entryPath], + outfile: outPath, + bundle: false, // Don't bundle - these are standalone scripts + format: 'cjs', // CommonJS for Electron preload + platform: 'node', + target: ['es2024'], + sourcemap: 'external', + sourcesContent: false, + minify: doMinify, + banner: { js: banner }, + logLevel: 'warning', + }); + })); + + console.log(`[standalone] Done`); +} + +async function copyCssFiles(outDir: string, excludeTests = false): Promise { + // Copy all CSS files from src to output (they're imported by JS) + const cssFiles = await globAsync('**/*.css', { + cwd: path.join(REPO_ROOT, SRC_DIR), + ignore: excludeTests ? ['**/test/**'] : [], + }); + + for (const file of cssFiles) { + const srcPath = path.join(REPO_ROOT, SRC_DIR, file); + const destPath = path.join(REPO_ROOT, outDir, file); + + await copyFile(srcPath, destPath); + } + + return cssFiles.length; +} + +async function copyResources(outDir: string, target: BuildTarget, excludeDevFiles = false, excludeTests = false): Promise { + console.log(`[resources] Copying to ${outDir} for target '${target}'...`); + let copied = 0; + + const ignorePatterns: string[] = []; + if (excludeTests) { + ignorePatterns.push('**/test/**'); + } + if (excludeDevFiles) { + ignorePatterns.push('**/*-dev.html'); + } + + const resourcePatterns = getResourcePatternsForTarget(target); + for (const pattern of resourcePatterns) { + const files = await globAsync(pattern, { + cwd: path.join(REPO_ROOT, SRC_DIR), + ignore: ignorePatterns, + }); + + for (const file of files) { + const srcPath = path.join(REPO_ROOT, SRC_DIR, file); + const destPath = path.join(REPO_ROOT, outDir, file); + + await copyFile(srcPath, destPath); + copied++; + } + } + + // Copy test fixtures (only for development builds) + if (!excludeTests) { + for (const pattern of testFixturePatterns) { + const files = await globAsync(pattern, { + cwd: path.join(REPO_ROOT, SRC_DIR), + }); + + for (const file of files) { + const srcPath = path.join(REPO_ROOT, SRC_DIR, file); + const destPath = path.join(REPO_ROOT, outDir, file); + + await copyFile(srcPath, destPath); + copied++; + } + } + } + + // Copy dev-only resources (vendor JS, codicon font) - only for development/transpile + // builds. In production bundles these are inlined by esbuild. + if (!excludeDevFiles) { + for (const pattern of devOnlyResourcePatterns) { + const files = await globAsync(pattern, { + cwd: path.join(REPO_ROOT, SRC_DIR), + ignore: ignorePatterns, + }); + for (const file of files) { + await copyFile(path.join(REPO_ROOT, SRC_DIR, file), path.join(REPO_ROOT, outDir, file)); + copied++; + } + } + } + + // Copy CSS files (only for development/transpile builds, not production bundles + // where CSS is already bundled into combined files like workbench.desktop.main.css) + if (!excludeDevFiles) { + const cssCount = await copyCssFiles(outDir, excludeTests); + copied += cssCount; + console.log(`[resources] Copied ${copied} files (${cssCount} CSS)`); + } else { + console.log(`[resources] Copied ${copied} files (CSS skipped - bundled)`); + } +} + +// ============================================================================ +// Plugins +// ============================================================================ + +function inlineMinimistPlugin(): esbuild.Plugin { + return { + name: 'inline-minimist', + setup(build) { + build.onResolve({ filter: /^minimist$/ }, () => ({ + path: path.join(REPO_ROOT, 'node_modules/minimist/index.js'), + external: false, + })); + }, + }; +} + +function cssExternalPlugin(): esbuild.Plugin { + // Mark CSS imports as external so they stay as import statements + // The CSS files are copied separately and loaded by the browser at runtime + return { + name: 'css-external', + setup(build) { + build.onResolve({ filter: /\.css$/ }, (args) => ({ + path: args.path, + external: true, + })); + }, + }; +} + +/** + * esbuild plugin that transforms source files to inject build-time configuration. + * This runs during onLoad so the transformation happens before esbuild processes the content, + * ensuring placeholders like `/*BUILD->INSERT_PRODUCT_CONFIGURATION* /` are replaced + * before esbuild strips them as non-legal comments. + */ +function fileContentMapperPlugin(outDir: string, target: BuildTarget): esbuild.Plugin { + // Cache the replacement strings (computed once) + let productConfigReplacement: string | undefined; + let builtinExtensionsReplacement: string | undefined; + + return { + name: 'file-content-mapper', + setup(build) { + build.onLoad({ filter: /\.ts$/ }, async (args) => { + // Skip .d.ts files + if (args.path.endsWith('.d.ts')) { + return undefined; + } + + let contents = await fs.promises.readFile(args.path, 'utf-8'); + let modified = false; + + // Inject product configuration + if (contents.includes('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/')) { + if (productConfigReplacement === undefined) { + // For server-web, remove webEndpointUrlTemplate + const productForTarget = target === 'server-web' + ? { ...product, webEndpointUrlTemplate: undefined } + : product; + const productConfiguration = JSON.stringify({ + ...productForTarget, + version, + commit, + date: readISODate(outDir) + }); + // Remove the outer braces since the placeholder is inside an object literal + productConfigReplacement = productConfiguration.substring(1, productConfiguration.length - 1); + } + contents = contents.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfigReplacement!); + modified = true; + } + + // Inject built-in extensions list + if (contents.includes('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/')) { + if (builtinExtensionsReplacement === undefined) { + // Web target uses .build/web/extensions (from compileWebExtensionsBuildTask) + // Other targets use .build/extensions + const extensionsRoot = target === 'web' ? '.build/web/extensions' : '.build/extensions'; + const builtinExtensions = JSON.stringify(scanBuiltinExtensions(extensionsRoot)); + // Remove the outer brackets since the placeholder is inside an array literal + builtinExtensionsReplacement = builtinExtensions.substring(1, builtinExtensions.length - 1); + } + contents = contents.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', () => builtinExtensionsReplacement!); + modified = true; + } + + if (modified) { + return { contents, loader: 'ts' }; + } + + // No modifications, let esbuild handle normally + return undefined; + }); + }, + }; +} + +// ============================================================================ +// Transpile (Goal 1: TS → JS using esbuild.transform for maximum speed) +// ============================================================================ + +// Shared transform options for single-file transpilation +const transformOptions: esbuild.TransformOptions = { + loader: 'ts', + format: 'esm', + target: 'es2024', + sourcemap: 'inline', + sourcesContent: false, + tsconfigRaw: JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + useDefineForClassFields: false + } + }), +}; + +async function transpileFile(srcPath: string, destPath: string, relativePath: string): Promise { + const source = await fs.promises.readFile(srcPath, 'utf-8'); + const result = await esbuild.transform(source, { + ...transformOptions, + sourcefile: relativePath, + }); + + await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); + await fs.promises.writeFile(destPath, result.code); +} + +async function transpile(outDir: string, excludeTests: boolean): Promise { + // Find all .ts files + const ignorePatterns = ['**/*.d.ts']; + if (excludeTests) { + ignorePatterns.push('**/test/**'); + } + + const files = await globAsync('**/*.ts', { + cwd: path.join(REPO_ROOT, SRC_DIR), + ignore: ignorePatterns, + }); + + console.log(`[transpile] Found ${files.length} files`); + + // Transpile all files in parallel using esbuild.transform (fastest approach) + await Promise.all(files.map(file => { + const srcPath = path.join(REPO_ROOT, SRC_DIR, file); + const destPath = path.join(REPO_ROOT, outDir, file.replace(/\.ts$/, '.js')); + return transpileFile(srcPath, destPath, file); + })); +} + +// ============================================================================ +// Bundle (Goal 2: JS → bundled JS) +// ============================================================================ + +async function bundle(outDir: string, doMinify: boolean, doNls: boolean, target: BuildTarget, sourceMapBaseUrl?: string): Promise { + await cleanDir(outDir); + + // Write build date file (used by packaging to embed in product.json) + const outDirPath = path.join(REPO_ROOT, outDir); + await fs.promises.mkdir(outDirPath, { recursive: true }); + await fs.promises.writeFile(path.join(outDirPath, 'date'), new Date().toISOString(), 'utf8'); + + console.log(`[bundle] ${SRC_DIR} → ${outDir} (target: ${target})${doMinify ? ' (minify)' : ''}${doNls ? ' (nls)' : ''}`); + const t1 = Date.now(); + + // Read TSLib for banner + const tslibPath = path.join(REPO_ROOT, 'node_modules/tslib/tslib.es6.js'); + const tslib = await fs.promises.readFile(tslibPath, 'utf-8'); + const banner = { + js: `/*!-------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +${tslib}`, + css: `/*!-------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/`, + }; + + // Shared TypeScript options for bundling directly from source + const tsconfigRaw = JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + useDefineForClassFields: false + } + }); + + // Create shared NLS collector (only used if doNls is true) + const nlsCollector = createNLSCollector(); + const preserveEnglish = false; // Production mode: replace messages with null + + // Get entry points based on target + const allEntryPoints = getEntryPointsForTarget(target); + const bootstrapEntryPoints = getBootstrapEntryPointsForTarget(target); + const bundleCssEntryPoints = getCssBundleEntryPointsForTarget(target); + + // Collect all build results (with write: false) + const buildResults: { outPath: string; result: esbuild.BuildResult }[] = []; + + // Create the file content mapper plugin (injects product config, builtin extensions) + const contentMapperPlugin = fileContentMapperPlugin(outDir, target); + + // Bundle each entry point directly from TypeScript source + await Promise.all(allEntryPoints.map(async (entryPoint) => { + const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entryPoint}.ts`); + const outPath = path.join(REPO_ROOT, outDir, `${entryPoint}.js`); + + // Use CSS external plugin for entry points that don't need bundled CSS + const plugins: esbuild.Plugin[] = bundleCssEntryPoints.has(entryPoint) ? [] : [cssExternalPlugin()]; + // Add content mapper plugin to inject product config and builtin extensions + plugins.push(contentMapperPlugin); + if (doNls) { + plugins.unshift(nlsPlugin({ + baseDir: path.join(REPO_ROOT, SRC_DIR), + collector: nlsCollector, + })); + } + + // For entry points that bundle CSS, we need to use outdir instead of outfile + // because esbuild can't produce multiple output files (JS + CSS) with outfile + const needsCssBundling = bundleCssEntryPoints.has(entryPoint); + + const buildOptions: esbuild.BuildOptions = { + entryPoints: needsCssBundling + ? [{ in: entryPath, out: entryPoint }] + : [entryPath], + ...(needsCssBundling + ? { outdir: path.join(REPO_ROOT, outDir) } + : { outfile: outPath }), + bundle: true, + format: 'esm', + platform: 'neutral', + target: ['es2024'], + packages: 'external', + sourcemap: 'external', + sourcesContent: true, + minify: doMinify, + treeShaking: true, + banner, + loader: { + '.ttf': 'file', + '.svg': 'file', + '.png': 'file', + '.sh': 'file', + }, + assetNames: 'media/[name]', + plugins, + write: false, // Don't write yet, we need to post-process + logLevel: 'warning', + logOverride: { + 'unsupported-require-call': 'silent', + }, + tsconfigRaw, + }; + + const result = await esbuild.build(buildOptions); + + buildResults.push({ outPath, result }); + })); + + // Bundle bootstrap files (with minimist inlined) directly from TypeScript source + for (const entry of bootstrapEntryPoints) { + const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entry}.ts`); + if (!fs.existsSync(entryPath)) { + console.log(`[bundle] Skipping ${entry} (not found)`); + continue; + } + + const outPath = path.join(REPO_ROOT, outDir, `${entry}.js`); + + const bootstrapPlugins: esbuild.Plugin[] = [inlineMinimistPlugin(), contentMapperPlugin]; + if (doNls) { + bootstrapPlugins.unshift(nlsPlugin({ + baseDir: path.join(REPO_ROOT, SRC_DIR), + collector: nlsCollector, + })); + } + + const result = await esbuild.build({ + entryPoints: [entryPath], + outfile: outPath, + bundle: true, + format: 'esm', + platform: 'node', + target: ['es2024'], + packages: 'external', + sourcemap: 'external', + sourcesContent: true, + minify: doMinify, + treeShaking: true, + banner, + plugins: bootstrapPlugins, + write: false, // Don't write yet, we need to post-process + logLevel: 'warning', + logOverride: { + 'unsupported-require-call': 'silent', + }, + tsconfigRaw, + }); + + buildResults.push({ outPath, result }); + } + + // Finalize NLS: sort entries, assign indices, write metadata files + let indexMap = new Map(); + if (doNls) { + // Also write NLS files to out-build for backwards compatibility with test runner + const nlsResult = await finalizeNLS( + nlsCollector, + path.join(REPO_ROOT, outDir), + [path.join(REPO_ROOT, 'out-build')] + ); + indexMap = nlsResult.indexMap; + } + + // Post-process and write all output files + let bundled = 0; + for (const { result } of buildResults) { + if (!result.outputFiles) { + continue; + } + + for (const file of result.outputFiles) { + await fs.promises.mkdir(path.dirname(file.path), { recursive: true }); + + if (file.path.endsWith('.js') || file.path.endsWith('.css')) { + let content = file.text; + + // Apply NLS post-processing if enabled (JS only) + if (file.path.endsWith('.js') && doNls && indexMap.size > 0) { + content = postProcessNLS(content, indexMap, preserveEnglish); + } + + // Rewrite sourceMappingURL to CDN URL if configured + if (sourceMapBaseUrl) { + const relativePath = path.relative(path.join(REPO_ROOT, outDir), file.path); + content = content.replace( + /\/\/# sourceMappingURL=.+$/m, + `//# sourceMappingURL=${sourceMapBaseUrl}/${relativePath}.map` + ); + content = content.replace( + /\/\*# sourceMappingURL=.+\*\/$/m, + `/*# sourceMappingURL=${sourceMapBaseUrl}/${relativePath}.map*/` + ); + } + + await fs.promises.writeFile(file.path, content); + } else { + // Write other files (source maps, assets) as-is + await fs.promises.writeFile(file.path, file.contents); + } + } + bundled++; + } + + // Copy resources (exclude dev files and tests for production) + await copyResources(outDir, target, true, true); + + // Compile standalone TypeScript files (like Electron preload scripts) that cannot be bundled + await compileStandaloneFiles(outDir, doMinify, target); + + console.log(`[bundle] Done in ${Date.now() - t1}ms (${bundled} bundles)`); +} + +// ============================================================================ +// Watch Mode +// ============================================================================ + +async function watch(): Promise { + if (!useEsbuildTranspile) { + console.log('Starting transpilation...'); + console.log('Finished transpilation with 0 errors after 0 ms'); + console.log('[watch] esbuild transpile disabled (useEsbuildTranspile=false). Keeping process alive as no-op.'); + await new Promise(() => { }); // keep alive + return; + } + + console.log('Starting transpilation...'); + + const outDir = OUT_DIR; + + // Initial setup + await cleanDir(outDir); + console.log(`[transpile] ${SRC_DIR} → ${outDir}`); + + // Initial full build + const t1 = Date.now(); + try { + await transpile(outDir, false); + await copyResources(outDir, 'desktop', false, false); + console.log(`Finished transpilation with 0 errors after ${Date.now() - t1} ms`); + } catch (err) { + console.error('[watch] Initial build failed:', err); + console.log(`Finished transpilation with 1 errors after ${Date.now() - t1} ms`); + // Continue watching anyway + } + + let pendingTsFiles: Set = new Set(); + let pendingCopyFiles: Set = new Set(); + + const processChanges = async () => { + console.log('Starting transpilation...'); + const t1 = Date.now(); + const tsFiles = [...pendingTsFiles]; + const filesToCopy = [...pendingCopyFiles]; + pendingTsFiles = new Set(); + pendingCopyFiles = new Set(); + + try { + // Transform changed TypeScript files in parallel + if (tsFiles.length > 0) { + console.log(`[watch] Transpiling ${tsFiles.length} file(s)...`); + await Promise.all(tsFiles.map(srcPath => { + const relativePath = path.relative(path.join(REPO_ROOT, SRC_DIR), srcPath); + const destPath = path.join(REPO_ROOT, outDir, relativePath.replace(/\.ts$/, '.js')); + return transpileFile(srcPath, destPath, relativePath); + })); + } + + // Copy changed resource files in parallel + if (filesToCopy.length > 0) { + await Promise.all(filesToCopy.map(async (srcPath) => { + const relativePath = path.relative(path.join(REPO_ROOT, SRC_DIR), srcPath); + const destPath = path.join(REPO_ROOT, outDir, relativePath); + await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); + await fs.promises.copyFile(srcPath, destPath); + console.log(`[watch] Copied ${relativePath}`); + })); + } + + if (tsFiles.length > 0 || filesToCopy.length > 0) { + console.log(`Finished transpilation with 0 errors after ${Date.now() - t1} ms`); + } + } catch (err) { + console.error('[watch] Rebuild failed:', err); + console.log(`Finished transpilation with 1 errors after ${Date.now() - t1} ms`); + // Continue watching + } + }; + + // Extensions to watch and copy (non-TypeScript resources) + const copyExtensions = ['.css', '.html', '.js', '.json', '.ttf', '.svg', '.png', '.mp3', '.scm', '.sh', '.ps1', '.psm1', '.fish', '.zsh', '.scpt']; + + // Watch src directory using existing gulp-watch based watcher + let debounceTimer: ReturnType | undefined; + const srcDir = path.join(REPO_ROOT, SRC_DIR); + const watchStream = gulpWatch('src/**', { base: srcDir, readDelay: 200 }); + + watchStream.on('data', (file: { path: string }) => { + if (file.path.endsWith('.ts') && !file.path.endsWith('.d.ts')) { + pendingTsFiles.add(file.path); + } else if (copyExtensions.some(ext => file.path.endsWith(ext))) { + pendingCopyFiles.add(file.path); + } + + if (pendingTsFiles.size > 0 || pendingCopyFiles.size > 0) { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(processChanges, 200); + } + }); + + console.log('[watch] Watching src/**/*.{ts,css,...} (Ctrl+C to stop)'); + + // Keep process alive + process.on('SIGINT', () => { + console.log('\n[watch] Stopping...'); + watchStream.end(); + process.exit(0); + }); +} + +// ============================================================================ +// Main +// ============================================================================ + +function printUsage(): void { + console.log(`Usage: npx tsx build/next/index.ts [options] + +Commands: + transpile Transpile TypeScript to JavaScript (single-file, fast) + bundle Bundle entry points into optimized bundles + +Options for 'transpile': + --watch Watch for changes and rebuild incrementally + --out Output directory (default: out) + --exclude-tests Exclude test files from transpilation + +Options for 'bundle': + --minify Minify the output bundles + --nls Process NLS (localization) strings + --out Output directory (default: out-vscode) + --target Build target: desktop (default), server, server-web, web + --source-map-base-url Rewrite sourceMappingURL to CDN URL + +Examples: + npx tsx build/next/index.ts transpile + npx tsx build/next/index.ts transpile --watch + npx tsx build/next/index.ts transpile --out out-build + npx tsx build/next/index.ts transpile --out out-build --exclude-tests + npx tsx build/next/index.ts bundle + npx tsx build/next/index.ts bundle --minify --nls + npx tsx build/next/index.ts bundle --nls --out out-vscode-min + npx tsx build/next/index.ts bundle --minify --nls --target server --out out-vscode-reh-min + npx tsx build/next/index.ts bundle --minify --nls --target server-web --out out-vscode-reh-web-min +`); +} + +async function main(): Promise { + const t1 = Date.now(); + + try { + switch (command) { + case 'transpile': + if (options.watch) { + await watch(); + } else { + const outDir = options.out ?? OUT_DIR; + await cleanDir(outDir); + + // Write build date file (used by packaging to embed in product.json) + const outDirPath = path.join(REPO_ROOT, outDir); + await fs.promises.mkdir(outDirPath, { recursive: true }); + await fs.promises.writeFile(path.join(outDirPath, 'date'), new Date().toISOString(), 'utf8'); + + console.log(`[transpile] ${SRC_DIR} → ${outDir}${options.excludeTests ? ' (excluding tests)' : ''}`); + const t1 = Date.now(); + await transpile(outDir, options.excludeTests); + await copyResources(outDir, 'desktop', false, options.excludeTests); + console.log(`[transpile] Done in ${Date.now() - t1}ms`); + } + break; + + case 'bundle': + await bundle(options.out ?? OUT_VSCODE_DIR, options.minify, options.nls, options.target as BuildTarget, options.sourceMapBaseUrl); + break; + + default: + printUsage(); + process.exit(command ? 1 : 0); + } + + if (!options.watch) { + console.log(`\n✓ Total: ${Date.now() - t1}ms`); + } + } catch (err) { + console.error('Build failed:', err); + process.exit(1); + } +} + +main(); diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts new file mode 100644 index 00000000000..56cb84fa33a --- /dev/null +++ b/build/next/nls-plugin.ts @@ -0,0 +1,319 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as esbuild from 'esbuild'; +import * as path from 'path'; +import * as fs from 'fs'; +import { + TextModel, + analyzeLocalizeCalls, + parseLocalizeKeyOrValue +} from '../lib/nls-analysis.ts'; + +// ============================================================================ +// Types +// ============================================================================ + +interface NLSEntry { + moduleId: string; + key: string | { key: string; comment: string[] }; + message: string; + placeholder: string; +} + +export interface NLSPluginOptions { + /** + * Base path for computing module IDs (e.g., 'src') + */ + baseDir: string; + + /** + * Shared collector for NLS entries across multiple builds. + * Create with createNLSCollector() and pass to multiple plugin instances. + */ + collector: NLSCollector; +} + +/** + * Collector for NLS entries across multiple esbuild builds. + */ +export interface NLSCollector { + entries: Map; + add(entry: NLSEntry): void; +} + +/** + * Creates a shared NLS collector that can be passed to multiple plugin instances. + */ +export function createNLSCollector(): NLSCollector { + const entries = new Map(); + return { + entries, + add(entry: NLSEntry) { + entries.set(entry.placeholder, entry); + } + }; +} + +/** + * Finalizes NLS collection and writes output files. + * Call this after all esbuild builds have completed. + */ +export async function finalizeNLS( + collector: NLSCollector, + outDir: string, + alsoWriteTo?: string[] +): Promise<{ indexMap: Map; messageCount: number }> { + if (collector.entries.size === 0) { + return { indexMap: new Map(), messageCount: 0 }; + } + + // Sort entries by moduleId, then by key for stable indices + const sortedEntries = [...collector.entries.values()].sort((a, b) => { + const aKey = typeof a.key === 'string' ? a.key : a.key.key; + const bKey = typeof b.key === 'string' ? b.key : b.key.key; + const moduleCompare = a.moduleId.localeCompare(b.moduleId); + if (moduleCompare !== 0) { + return moduleCompare; + } + return aKey.localeCompare(bKey); + }); + + // Create index map + const indexMap = new Map(); + sortedEntries.forEach((entry, idx) => { + indexMap.set(entry.placeholder, idx); + }); + + // Build NLS metadata + const allMessages: string[] = []; + const moduleToKeys: Map = new Map(); + const moduleToMessages: Map = new Map(); + + for (const entry of sortedEntries) { + allMessages.push(entry.message); + + if (!moduleToKeys.has(entry.moduleId)) { + moduleToKeys.set(entry.moduleId, []); + moduleToMessages.set(entry.moduleId, []); + } + moduleToKeys.get(entry.moduleId)!.push(entry.key); + moduleToMessages.get(entry.moduleId)!.push(entry.message); + } + + // nls.keys.json: [["moduleId", ["key1", "key2"]], ...] + const nlsKeysJson: [string, string[]][] = []; + for (const [moduleId, keys] of moduleToKeys) { + nlsKeysJson.push([moduleId, keys.map(k => typeof k === 'string' ? k : k.key)]); + } + + // nls.metadata.json: { keys: {...}, messages: {...} } + const nlsMetadataJson = { + keys: Object.fromEntries(moduleToKeys), + messages: Object.fromEntries(moduleToMessages) + }; + + // Write NLS files + const allOutDirs = [outDir, ...(alsoWriteTo ?? [])]; + for (const dir of allOutDirs) { + await fs.promises.mkdir(dir, { recursive: true }); + } + + await Promise.all(allOutDirs.flatMap(dir => [ + fs.promises.writeFile( + path.join(dir, 'nls.messages.json'), + JSON.stringify(allMessages) + ), + fs.promises.writeFile( + path.join(dir, 'nls.keys.json'), + JSON.stringify(nlsKeysJson) + ), + fs.promises.writeFile( + path.join(dir, 'nls.metadata.json'), + JSON.stringify(nlsMetadataJson, null, '\t') + ), + fs.promises.writeFile( + path.join(dir, 'nls.messages.js'), + `/*---------------------------------------------------------\n * Copyright (C) Microsoft Corporation. All rights reserved.\n *--------------------------------------------------------*/\nglobalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(allMessages)};` + ), + ])); + + console.log(`[nls] Extracted ${allMessages.length} messages from ${moduleToKeys.size} modules`); + + return { indexMap, messageCount: allMessages.length }; +} + +/** + * Post-processes a JavaScript file to replace NLS placeholders with indices. + */ +export function postProcessNLS( + content: string, + indexMap: Map, + preserveEnglish: boolean +): string { + return replaceInOutput(content, indexMap, preserveEnglish); +} + +// ============================================================================ +// Transformation +// ============================================================================ + +function transformToPlaceholders( + source: string, + moduleId: string +): { code: string; entries: NLSEntry[] } { + const localizeCalls = analyzeLocalizeCalls(source, 'localize'); + const localize2Calls = analyzeLocalizeCalls(source, 'localize2'); + + // Tag calls with their type so we can handle them differently later + const taggedLocalize = localizeCalls.map(call => ({ call, isLocalize2: false })); + const taggedLocalize2 = localize2Calls.map(call => ({ call, isLocalize2: true })); + const allCalls = [...taggedLocalize, ...taggedLocalize2].sort( + (a, b) => a.call.keySpan.start.line - b.call.keySpan.start.line || + a.call.keySpan.start.character - b.call.keySpan.start.character + ); + + if (allCalls.length === 0) { + return { code: source, entries: [] }; + } + + const entries: NLSEntry[] = []; + const model = new TextModel(source); + + // Process in reverse order to preserve positions + for (const { call, isLocalize2 } of allCalls.reverse()) { + const keyParsed = parseLocalizeKeyOrValue(call.key) as string | { key: string; comment: string[] }; + const messageParsed = parseLocalizeKeyOrValue(call.value); + const keyString = typeof keyParsed === 'string' ? keyParsed : keyParsed.key; + + // Use different placeholder prefix for localize vs localize2 + // localize: message will be replaced with null + // localize2: message will be preserved (only key replaced) + const prefix = isLocalize2 ? 'NLS2' : 'NLS'; + const placeholder = `%%${prefix}:${moduleId}#${keyString}%%`; + + entries.push({ + moduleId, + key: keyParsed, + message: String(messageParsed), + placeholder + }); + + // Replace the key with the placeholder string + model.apply(call.keySpan, `"${placeholder}"`); + } + + // Reverse entries to match source order + entries.reverse(); + + return { code: model.toString(), entries }; +} + +function replaceInOutput( + content: string, + indexMap: Map, + preserveEnglish: boolean +): string { + // Replace all placeholders in a single pass using regex + // Two types of placeholders: + // - %%NLS:moduleId#key%% for localize() - message replaced with null + // - %%NLS2:moduleId#key%% for localize2() - message preserved + // Note: esbuild may use single or double quotes, so we handle both + + if (preserveEnglish) { + // Just replace the placeholder with the index (both NLS and NLS2) + return content.replace(/["']%%NLS2?:([^%]+)%%["']/g, (match, inner) => { + // Try NLS first, then NLS2 + let placeholder = `%%NLS:${inner}%%`; + let index = indexMap.get(placeholder); + if (index === undefined) { + placeholder = `%%NLS2:${inner}%%`; + index = indexMap.get(placeholder); + } + if (index !== undefined) { + return String(index); + } + // Placeholder not found in map, leave as-is (shouldn't happen) + return match; + }); + } else { + // For NLS (localize): replace placeholder with index AND replace message with null + // For NLS2 (localize2): replace placeholder with index, keep message + // Note: Use (?:[^"\\]|\\.)* to properly handle escaped quotes like \" or \\ + // Note: esbuild may use single or double quotes, so we handle both + + // First handle NLS (localize) - replace both key and message + content = content.replace( + /["']%%NLS:([^%]+)%%["'](\s*,\s*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, + (match, inner, comma) => { + const placeholder = `%%NLS:${inner}%%`; + const index = indexMap.get(placeholder); + if (index !== undefined) { + return `${index}${comma}null`; + } + return match; + } + ); + + // Then handle NLS2 (localize2) - replace only key, keep message + content = content.replace( + /["']%%NLS2:([^%]+)%%["']/g, + (match, inner) => { + const placeholder = `%%NLS2:${inner}%%`; + const index = indexMap.get(placeholder); + if (index !== undefined) { + return String(index); + } + return match; + } + ); + + return content; + } +} + +// ============================================================================ +// Plugin +// ============================================================================ + +export function nlsPlugin(options: NLSPluginOptions): esbuild.Plugin { + const { collector } = options; + + return { + name: 'nls', + setup(build) { + // Transform TypeScript files to replace localize() calls with placeholders + build.onLoad({ filter: /\.ts$/ }, async (args) => { + // Skip .d.ts files + if (args.path.endsWith('.d.ts')) { + return undefined; + } + + const source = await fs.promises.readFile(args.path, 'utf-8'); + + // Compute module ID (e.g., "vs/editor/editor" from "src/vs/editor/editor.ts") + const relativePath = path.relative(options.baseDir, args.path); + const moduleId = relativePath + .replace(/\\/g, '/') + .replace(/\.ts$/, ''); + + // Transform localize() calls to placeholders + const { code, entries: fileEntries } = transformToPlaceholders(source, moduleId); + + // Collect entries + for (const entry of fileEntries) { + collector.add(entry); + } + + if (fileEntries.length > 0) { + return { contents: code, loader: 'ts' }; + } + + // No NLS calls, return undefined to let esbuild handle normally + return undefined; + }); + } + }; +} diff --git a/build/next/working.md b/build/next/working.md new file mode 100644 index 00000000000..bbf23a99806 --- /dev/null +++ b/build/next/working.md @@ -0,0 +1,235 @@ +# Working Notes: New esbuild-based Build System + +> These notes are for AI agents to help with context in new or summarized sessions. + +## Important: Validating Changes + +**The `VS Code - Build` task is NOT needed to validate changes in the `build/` folder!** + +Build scripts in `build/` are TypeScript files that run directly with `tsx` (e.g., `npx tsx build/next/index.ts`). They are not compiled by the main VS Code build. + +To test changes: +```bash +# Test transpile +npx tsx build/next/index.ts transpile --out out-test + +# Test bundle (server-web target to test the auth fix) +npx tsx build/next/index.ts bundle --nls --target server-web --out out-vscode-reh-web-test + +# Verify product config was injected +grep -l "serverLicense" out-vscode-reh-web-test/vs/code/browser/workbench/workbench.js +``` + +--- + +## Architecture Overview + +### Files + +- **[index.ts](index.ts)** - Main build orchestrator + - `transpile` command: Fast TS → JS using `esbuild.transform()` + - `bundle` command: TS → bundled JS using `esbuild.build()` +- **[nls-plugin.ts](nls-plugin.ts)** - NLS (localization) esbuild plugin + +### Integration with Old Build + +In [gulpfile.vscode.ts](../gulpfile.vscode.ts#L228-L242), the `core-ci` task uses these new scripts: +- `runEsbuildTranspile()` → transpile command +- `runEsbuildBundle()` → bundle command + +Old gulp-based bundling renamed to `core-ci-OLD`. + +--- + +## Key Learnings + +### 1. Comment Stripping by esbuild + +**Problem:** esbuild strips comments like `/*BUILD->INSERT_PRODUCT_CONFIGURATION*/` during bundling. + +**Solution:** Use an `onLoad` plugin to transform source files BEFORE esbuild processes them. See `fileContentMapperPlugin()` in index.ts. + +**Why post-processing doesn't work:** By the time we post-process the bundled output, the comment placeholder has already been stripped. + +### 2. Authorization Error: "Unauthorized client refused" + +**Root cause:** Missing product configuration in browser bundle. + +**Flow:** +1. Browser loads with empty product config (placeholder was stripped) +2. `productService.serverLicense` is empty/undefined +3. Browser's `SignService.vsda()` can't decrypt vsda WASM (needs serverLicense as key) +4. Browser's `sign()` returns original challenge instead of signed value +5. Server validates signature → fails +6. Server is in built mode (no `VSCODE_DEV`) → rejects connection + +**Fix:** The `fileContentMapperPlugin` now runs during `onLoad`, replacing placeholders before esbuild strips them. + +### 3. Build-Time Placeholders + +Two placeholders that need injection: + +| Placeholder | Location | Purpose | +|-------------|----------|---------| +| `/*BUILD->INSERT_PRODUCT_CONFIGURATION*/` | `src/vs/platform/product/common/product.ts` | Product config (commit, version, serverLicense, etc.) | +| `/*BUILD->INSERT_BUILTIN_EXTENSIONS*/` | `src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts` | List of built-in extensions | + +### 4. Server-web Target Specifics + +- Removes `webEndpointUrlTemplate` from product config (see `tweakProductForServerWeb` in old build) +- Uses `.build/extensions` for builtin extensions (not `.build/web/extensions`) + +### 5. Entry Point Parity with Old Build + +**Problem:** The desktop target had `keyboardMapEntryPoints` as separate esbuild entry points, producing `layout.contribution.darwin.js`, `layout.contribution.linux.js`, and `layout.contribution.win.js` as standalone files in the output. + +**Root cause:** In the old build (`gulpfile.vscode.ts`), `vscodeEntryPoints` does NOT include `buildfile.keyboardMaps`. These files are only separate entry points for server-web (`gulpfile.reh.ts`) and web (`gulpfile.vscode.web.ts`). For desktop, they're imported as dependencies of `workbench.desktop.main` and get bundled into it. + +**Fix:** Removed `...keyboardMapEntryPoints` from the `desktop` case in `getEntryPointsForTarget()`. Keep for `server-web` and `web`. + +**Lesson:** Always verify new build entry points against the old build's per-target definitions in `buildfile.ts` and the respective gulpfiles. + +### 6. NLS Output File Parity + +**Problem:** `finalizeNLS()` was generating `nls.messages.js` (with `globalThis._VSCODE_NLS_MESSAGES=...`) in addition to the standard `.json` files. The old build only produces `nls.messages.json`, `nls.keys.json`, and `nls.metadata.json`. + +**Fix:** Removed `nls.messages.js` generation from `finalizeNLS()` in `nls-plugin.ts`. + +**Lesson:** Don't add new output file formats that create parity differences with the old build. The old build is the reference. + +--- + +## Testing the Fix + +```bash +# Build server-web with new system +npx tsx build/next/index.ts bundle --nls --target server-web --out out-vscode-reh-web-min + +# Package it (uses gulp task) +npm run gulp vscode-reh-web-darwin-arm64-min + +# Run server +./vscode-server-darwin-arm64-web/bin/code-server-oss --connection-token dev-token + +# Open browser - should connect without "Unauthorized client refused" +``` + +--- + +## Open Items / Future Work + +1. **`BUILD_INSERT_PACKAGE_CONFIGURATION`** - Server bootstrap files ([bootstrap-meta.ts](../../src/bootstrap-meta.ts)) have this marker for package.json injection. Currently handled by [inlineMeta.ts](../lib/inlineMeta.ts) in the old build's packaging step. + +2. **Mangling** - The new build doesn't do TypeScript-based mangling yet. Old `core-ci` with mangling is now `core-ci-OLD`. + +3. **Entry point duplication** - Entry points are duplicated between [buildfile.ts](../buildfile.ts) and [index.ts](index.ts). Consider consolidating. + +--- + +## Build Comparison: OLD (gulp-tsb) vs NEW (esbuild) — Desktop Build + +### Summary + +| Metric | OLD | NEW | Delta | +|--------|-----|-----|-------| +| Total files in `out/` | 3993 | 4301 | +309 extra, 1 missing | +| Total size of `out/` | 25.8 MB | 64.6 MB | +38.8 MB (2.5×) | +| `workbench.desktop.main.js` | 13.0 MB | 15.5 MB | +2.5 MB | + +### 1 Missing File (in OLD, not in NEW) + +| File | Why Missing | Fix | +|------|-------------|-----| +| `out/vs/platform/browserView/electron-browser/preload-browserView.js` | Not listed in `desktopStandaloneFiles` in index.ts. Only `preload.ts` and `preload-aux.ts` are compiled as standalone files. | **Add** `'vs/platform/browserView/electron-browser/preload-browserView.ts'` to the `desktopStandaloneFiles` array in `index.ts`. | + +### 309 Extra Files (in NEW, not in OLD) — Breakdown + +| Category | Count | Explanation | +|----------|-------|-------------| +| **CSS files** | 291 | `copyCssFiles()` copies ALL `.css` from `src/` to the output. The old bundler inlines CSS into the main `.css` bundle (e.g., `workbench.desktop.main.css`) and never ships individual CSS files. These individual files ARE needed at runtime because the new ESM system uses `import './foo.css'` resolved by an import map. | +| **Vendor JS files** | 3 | `dompurify.js`, `marked.js`, `semver.js` — listed in `commonResourcePatterns`. The old bundler inlines these into the main bundle. The new system keeps them as separate files because they're plain JS (not TS). They're needed. | +| **Web workbench bundle** | 1 | `vs/code/browser/workbench/workbench.js` (15.4 MB). This is the web workbench entry point bundle. It should NOT be in a desktop build — the old build explicitly excludes `out-build/vs/code/browser/**`. The `desktopResourcePatterns` in index.ts includes `vs/code/browser/workbench/*.html` and `callback.html` which is correct, but the actual bundle gets written by the esbuild desktop bundle step because the desktop entry points include web entry points. | +| **Web workbench internal** | 1 | `vs/workbench/workbench.web.main.internal.js` (15.4 MB). Similar: shouldn't ship in a desktop build. It's output by the esbuild bundler. | +| **Keyboard layout contributions** | 3 | `layout.contribution.{darwin,linux,win}.js` — the old bundler inlines these into the main bundle. These are new separate files from the esbuild bundler. | +| **NLS files** | 2 | `nls.messages.js` (new) and `nls.metadata.json` (new). The old build has `nls.messages.json` and `nls.keys.json` but not a `.js` version or metadata. The `.js` version is produced by the NLS plugin. | +| **HTML files** | 2 | `vs/code/browser/workbench/workbench.html` and `callback.html` — correctly listed in `desktopResourcePatterns` (these are needed for desktop's built-in web server). | +| **SVG loading spinners** | 3 | `loading-dark.svg`, `loading-hc.svg`, `loading.svg` in `vs/workbench/contrib/extensions/browser/media/`. The old build only copies `theme-icon.png` and `language-icon.svg` from that folder; the new build's `desktopResourcePatterns` uses `*.svg` which is broader. | +| **codicon.ttf (duplicate)** | 1 | At `vs/base/browser/ui/codicons/codicon/codicon.ttf`. The old build copies this to `out/media/codicon.ttf` only. The new build has BOTH: the copy in `out/media/` (from esbuild's `file` loader) AND the original path (from `commonResourcePatterns`). Duplicate. | +| **PSReadLine.psm1** | 1 | `vs/workbench/contrib/terminal/common/scripts/psreadline/PSReadLine.psm1` — the old build uses `*.psm1` in `terminal/common/scripts/` (non-recursive?). The new build uses `**/*.psm1` (recursive), picking up this subdirectory file. Check if it's needed. | +| **date file** | 1 | `out/date` — build timestamp, produced by the new build's `bundle()` function. The old build doesn't write this; it reads `package.json.date` instead. | + +### Size Increase Breakdown by Area + +| Area | OLD | NEW | Delta | Why | +|------|-----|-----|-------|-----| +| `vs/code` | 1.5 MB | 17.4 MB | +15.9 MB | Web workbench bundle (15.4 MB) shouldn't be in desktop build | +| `vs/workbench` | 18.9 MB | 38.7 MB | +19.8 MB | `workbench.web.main.internal.js` (15.4 MB) + unmangled desktop bundle (+2.5 MB) + individual CSS files (~1 MB) | +| `vs/base` | 0 MB | 0.4 MB | +0.4 MB | Individual CSS files + vendor JS | +| `vs/editor` | 0.3 MB | 0.5 MB | +0.1 MB | Individual CSS files | +| `vs/platform` | 1.7 MB | 1.9 MB | +0.2 MB | Individual CSS files | + +### JS Files with >2× Size Change + +| File | OLD | NEW | Ratio | Reason | +|------|-----|-----|-------|--------| +| `vs/workbench/contrib/webview/browser/pre/service-worker.js` | 7 KB | 15 KB | 2.2× | Not minified / includes more inlined code | +| `vs/code/electron-browser/workbench/workbench.js` | 10 KB | 28 KB | 2.75× | OLD is minified to 6 lines; NEW is 380 lines (not compressed, includes tslib banner) | + +### Action Items + +1. **[CRITICAL] Missing `preload-browserView.ts`** — Add to `desktopStandaloneFiles` in index.ts. Without it, BrowserView (used for Simple Browser) may fail. +2. **[SIZE] Web bundles in desktop build** — `workbench.web.main.internal.js` and `vs/code/browser/workbench/workbench.js` together add ~31 MB. These are written by the esbuild bundler and not filtered out. Consider: either don't bundle web entry points for the desktop target, or ensure the packaging step excludes them (currently `packageTask` takes `out-vscode-min/**` without filtering). +3. **[SIZE] No mangling** — The desktop main bundle is 2.5 MB larger due to no property mangling. Known open item. +4. **[MINOR] Duplicate codicon.ttf** — Exists at both `out/media/codicon.ttf` (from esbuild `file` loader) and `out/vs/base/browser/ui/codicons/codicon/codicon.ttf` (from `commonResourcePatterns`). Consider removing from `commonResourcePatterns` if it's already handled by the loader. +5. **[MINOR] Extra SVGs** — `desktopResourcePatterns` uses `*.svg` for extensions media but old build only ships `language-icon.svg`. The loading spinners may be unused in the desktop build. +6. **[MINOR] Extra PSReadLine.psm1** from recursive glob — verify if needed. + +--- + +## Source Maps + +### Fixes Applied + +1. **`sourcesContent: true`** — Production bundles now embed original TypeScript source content in `.map` files, matching the old build's `includeContent: true` behavior. Without this, crash reports from CDN-hosted source maps can't show original source. + +2. **`--source-map-base-url` option** — The `bundle` command accepts an optional `--source-map-base-url ` flag. When set, post-processing rewrites `sourceMappingURL` comments in `.js` and `.css` output files to point to the CDN (e.g., `https://main.vscode-cdn.net/sourcemaps//core/vs/...`). This matches the old build's `sourceMappingURL` function in `minifyTask()`. Wired up in `gulpfile.vscode.ts` for `core-ci-esbuild` and `vscode-esbuild-min` tasks. + +### NLS Source Map Accuracy (Decision: Accept Imprecision) + +**Problem:** `postProcessNLS()` replaces `"%%NLS:moduleId#key%%"` placeholders (~40 chars) with short index values like `null` (4 chars) in the final JS output. This shifts column positions without updating the `.map` files. + +**Options considered:** + +| Option | Description | Effort | Accuracy | +|--------|-------------|--------|----------| +| A. Fixed-width placeholders | Pad placeholders to match replacement length | Hard — indices unknown until all modules are collected across parallel bundles | Perfect | +| B. Post-process source map | Parse `.map`, track replacement offsets per line, adjust VLQ mappings | Medium | Perfect | +| C. Two-pass build | Assign NLS indices during plugin phase | Not feasible with parallel bundling | N/A | +| **D. Accept imprecision** | NLS replacements only affect column positions; line-level debugging works | Zero | Line-level | + +**Decision: Option D — accept imprecision.** Rationale: + +- NLS replacements only shift **columns**, never lines — line-level stack traces and breakpoints remain correct. +- Production crash reporting (the primary consumer of CDN source maps) uses line numbers; column-level accuracy is rarely needed. +- The old gulp build had the same fundamental issue in its `nls.nls()` step and used `SourceMapConsumer`/`SourceMapGenerator` to fix it — but that approach was fragile and slow. +- If column-level precision becomes important later (e.g., for minified+NLS bundles), Option B can be implemented: after NLS replacement, re-parse the source map, walk replacement sites, and adjust column offsets. This is a localized change in the post-processing loop. + +--- + +## Self-hosting Setup + +The default `VS Code - Build` task now runs three parallel watchers: + +| Task | What it does | Script | +|------|-------------|--------| +| **Core - Transpile** | esbuild single-file TS→JS (fast, no type checking) | `watch-client-transpiled` → `npx tsx build/next/index.ts transpile --watch` | +| **Core - Typecheck** | gulp-tsb `noEmit` watch (type errors only, no output) | `watch-clientd` → `gulp watch-client` (with `noEmit: true`) | +| **Ext - Build** | Extension compilation (unchanged) | `watch-extensionsd` | + +### Key Changes + +- **`compilation.ts`**: `ICompileTaskOptions` gained `noEmit?: boolean`. When set, `overrideOptions.noEmit = true` is passed to tsb. `watchTask()` accepts an optional 4th parameter `{ noEmit?: boolean }`. +- **`gulpfile.ts`**: `watchClientTask` no longer runs `rimraf('out')` (the transpiler owns that). Passes `{ noEmit: true }` to `watchTask`. +- **`index.ts`**: Watch mode emits `Starting transpilation...` / `Finished transpilation with N errors after X ms` for VS Code problem matcher. +- **`tasks.json`**: Old "Core - Build" split into "Core - Transpile" + "Core - Typecheck" with separate problem matchers (owners: `esbuild` vs `typescript`). +- **`package.json`**: Added `watch-client-transpile`, `watch-client-transpiled`, `kill-watch-client-transpiled` scripts. diff --git a/build/vite/fixtures/aiStats.fixture.ts b/build/vite/fixtures/aiStats.fixture.ts new file mode 100644 index 00000000000..5ebe414b52b --- /dev/null +++ b/build/vite/fixtures/aiStats.fixture.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { observableValue } from '../../../src/vs/base/common/observable'; +import { createAiStatsHover, IAiStatsHoverData } from '../../../src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar'; +import { ISessionData } from '../../../src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart'; +import { Random } from '../../../src/vs/editor/test/common/core/random'; +import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils'; + +export default defineThemedFixtureGroup({ + AiStatsHover: defineComponentFixture({ + render: (context) => renderAiStatsHover({ ...context, data: createSampleDataWithSessions() }), + }), + + AiStatsHoverNoData: defineComponentFixture({ + render: (context) => renderAiStatsHover({ ...context, data: createEmptyData() }), + }), +}); + +function createSampleDataWithSessions(): IAiStatsHoverData { + const random = Random.create(42); + + // Use a fixed base time for determinism (Jan 1, 2025, 12:00:00 UTC) + const baseTime = 1735732800000; + const dayMs = 24 * 60 * 60 * 1000; + const sessionLengthMs = 5 * 60 * 1000; + + // Generate fake session data for the last 7 days + const fakeSessions: ISessionData[] = []; + for (let day = 6; day >= 0; day--) { + const dayStart = baseTime - day * dayMs; + const sessionsPerDay = random.nextIntRange(3, 9); + for (let s = 0; s < sessionsPerDay; s++) { + const sessionTime = dayStart + s * sessionLengthMs * 2; + fakeSessions.push({ + startTime: sessionTime, + typedCharacters: random.nextIntRange(100, 600), + aiCharacters: random.nextIntRange(200, 1000), + acceptedInlineSuggestions: random.nextIntRange(1, 16), + chatEditCount: random.nextIntRange(0, 5), + }); + } + } + + const totalAi = fakeSessions.reduce((sum, s) => sum + s.aiCharacters, 0); + const totalTyped = fakeSessions.reduce((sum, s) => sum + s.typedCharacters, 0); + const aiRate = totalAi / (totalAi + totalTyped); + + // "Today" for the fixture is the baseTime day + const startOfToday = baseTime - (baseTime % dayMs); + const todaySessions = fakeSessions.filter(s => s.startTime >= startOfToday); + const acceptedToday = todaySessions.reduce((sum, s) => sum + (s.acceptedInlineSuggestions ?? 0), 0); + + return { + aiRate: observableValue('aiRate', aiRate), + acceptedInlineSuggestionsToday: observableValue('acceptedToday', acceptedToday), + sessions: observableValue('sessions', fakeSessions), + }; +} + +function createEmptyData(): IAiStatsHoverData { + return { + aiRate: observableValue('aiRate', 0), + acceptedInlineSuggestionsToday: observableValue('acceptedToday', 0), + sessions: observableValue('sessions', []), + }; +} + +interface RenderAiStatsOptions extends ComponentFixtureContext { + data: IAiStatsHoverData; +} + +function renderAiStatsHover({ container, disposableStore, data }: RenderAiStatsOptions): HTMLElement { + container.style.width = '320px'; + container.style.padding = '8px'; + container.style.backgroundColor = 'var(--vscode-editorHoverWidget-background)'; + container.style.border = '1px solid var(--vscode-editorHoverWidget-border)'; + container.style.borderRadius = '4px'; + container.style.color = 'var(--vscode-editorHoverWidget-foreground)'; + + const hover = createAiStatsHover({ + data, + onOpenSettings: () => console.log('Open settings clicked'), + }); + + const elem = hover.keepUpdated(disposableStore).element; + container.appendChild(elem); + + return container; +} diff --git a/build/vite/fixtures/baseUI.fixture.ts b/build/vite/fixtures/baseUI.fixture.ts new file mode 100644 index 00000000000..4bcf0be5235 --- /dev/null +++ b/build/vite/fixtures/baseUI.fixture.ts @@ -0,0 +1,531 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from '../../../src/vs/base/browser/dom'; +import { Codicon } from '../../../src/vs/base/common/codicons'; +import { ThemeIcon } from '../../../src/vs/base/common/themables'; +import { Action, Separator } from '../../../src/vs/base/common/actions'; + +// UI Components +import { Button, ButtonBar, ButtonWithDescription, unthemedButtonStyles } from '../../../src/vs/base/browser/ui/button/button'; +import { Toggle, Checkbox, unthemedToggleStyles } from '../../../src/vs/base/browser/ui/toggle/toggle'; +import { InputBox, MessageType, unthemedInboxStyles } from '../../../src/vs/base/browser/ui/inputbox/inputBox'; +import { CountBadge } from '../../../src/vs/base/browser/ui/countBadge/countBadge'; +import { ActionBar } from '../../../src/vs/base/browser/ui/actionbar/actionbar'; +import { ProgressBar } from '../../../src/vs/base/browser/ui/progressbar/progressbar'; +import { HighlightedLabel } from '../../../src/vs/base/browser/ui/highlightedlabel/highlightedLabel'; + +import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils'; + + +// ============================================================================ +// Styles (themed versions for fixture display) +// ============================================================================ + +const themedButtonStyles = { + ...unthemedButtonStyles, + buttonBackground: 'var(--vscode-button-background)', + buttonHoverBackground: 'var(--vscode-button-hoverBackground)', + buttonForeground: 'var(--vscode-button-foreground)', + buttonSecondaryBackground: 'var(--vscode-button-secondaryBackground)', + buttonSecondaryHoverBackground: 'var(--vscode-button-secondaryHoverBackground)', + buttonSecondaryForeground: 'var(--vscode-button-secondaryForeground)', + buttonBorder: 'var(--vscode-button-border)', +}; + +const themedToggleStyles = { + ...unthemedToggleStyles, + inputActiveOptionBorder: 'var(--vscode-inputOption-activeBorder)', + inputActiveOptionForeground: 'var(--vscode-inputOption-activeForeground)', + inputActiveOptionBackground: 'var(--vscode-inputOption-activeBackground)', +}; + +const themedCheckboxStyles = { + checkboxBackground: 'var(--vscode-checkbox-background)', + checkboxBorder: 'var(--vscode-checkbox-border)', + checkboxForeground: 'var(--vscode-checkbox-foreground)', + checkboxDisabledBackground: undefined, + checkboxDisabledForeground: undefined, +}; + +const themedInputBoxStyles = { + ...unthemedInboxStyles, + inputBackground: 'var(--vscode-input-background)', + inputForeground: 'var(--vscode-input-foreground)', + inputBorder: 'var(--vscode-input-border)', + inputValidationInfoBackground: 'var(--vscode-inputValidation-infoBackground)', + inputValidationInfoBorder: 'var(--vscode-inputValidation-infoBorder)', + inputValidationWarningBackground: 'var(--vscode-inputValidation-warningBackground)', + inputValidationWarningBorder: 'var(--vscode-inputValidation-warningBorder)', + inputValidationErrorBackground: 'var(--vscode-inputValidation-errorBackground)', + inputValidationErrorBorder: 'var(--vscode-inputValidation-errorBorder)', +}; + +const themedBadgeStyles = { + badgeBackground: 'var(--vscode-badge-background)', + badgeForeground: 'var(--vscode-badge-foreground)', + badgeBorder: undefined, +}; + +const themedProgressBarOptions = { + progressBarBackground: 'var(--vscode-progressBar-background)', +}; + + +// ============================================================================ +// Buttons +// ============================================================================ + +function renderButtons({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '12px'; + + // Section: Primary Buttons + const primarySection = $('div'); + primarySection.style.display = 'flex'; + primarySection.style.gap = '8px'; + primarySection.style.alignItems = 'center'; + container.appendChild(primarySection); + + const primaryButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'Primary button' })); + primaryButton.label = 'Primary Button'; + + const primaryIconButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'With Icon', supportIcons: true })); + primaryIconButton.label = '$(add) Add Item'; + + const smallButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'Small button', small: true })); + smallButton.label = 'Small'; + + // Section: Secondary Buttons + const secondarySection = $('div'); + secondarySection.style.display = 'flex'; + secondarySection.style.gap = '8px'; + secondarySection.style.alignItems = 'center'; + container.appendChild(secondarySection); + + const secondaryButton = disposableStore.add(new Button(secondarySection, { ...themedButtonStyles, secondary: true, title: 'Secondary button' })); + secondaryButton.label = 'Secondary Button'; + + const secondaryIconButton = disposableStore.add(new Button(secondarySection, { ...themedButtonStyles, secondary: true, title: 'Cancel', supportIcons: true })); + secondaryIconButton.label = '$(close) Cancel'; + + // Section: Disabled Buttons + const disabledSection = $('div'); + disabledSection.style.display = 'flex'; + disabledSection.style.gap = '8px'; + disabledSection.style.alignItems = 'center'; + container.appendChild(disabledSection); + + const disabledButton = disposableStore.add(new Button(disabledSection, { ...themedButtonStyles, title: 'Disabled', disabled: true })); + disabledButton.label = 'Disabled'; + disabledButton.enabled = false; + + const disabledSecondary = disposableStore.add(new Button(disabledSection, { ...themedButtonStyles, secondary: true, title: 'Disabled Secondary', disabled: true })); + disabledSecondary.label = 'Disabled Secondary'; + disabledSecondary.enabled = false; + + return container; +} + +function renderButtonBar({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '16px'; + + // Button Bar + const barContainer = $('div'); + container.appendChild(barContainer); + + const buttonBar = new ButtonBar(barContainer); + disposableStore.add(buttonBar); + + const okButton = buttonBar.addButton({ ...themedButtonStyles, title: 'OK' }); + okButton.label = 'OK'; + + const cancelButton = buttonBar.addButton({ ...themedButtonStyles, secondary: true, title: 'Cancel' }); + cancelButton.label = 'Cancel'; + + // Button with Description + const descContainer = $('div'); + descContainer.style.width = '300px'; + container.appendChild(descContainer); + + const buttonWithDesc = disposableStore.add(new ButtonWithDescription(descContainer, { ...themedButtonStyles, title: 'Install Extension', supportIcons: true })); + buttonWithDesc.label = '$(extensions) Install Extension'; + buttonWithDesc.description = 'This will install the extension and enable it globally'; + + return container; +} + + +// ============================================================================ +// Toggles and Checkboxes +// ============================================================================ + +function renderToggles({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '12px'; + + // Toggles + const toggleSection = $('div'); + toggleSection.style.display = 'flex'; + toggleSection.style.gap = '16px'; + toggleSection.style.alignItems = 'center'; + container.appendChild(toggleSection); + + const toggle1 = disposableStore.add(new Toggle({ + ...themedToggleStyles, + title: 'Case Sensitive', + isChecked: false, + icon: Codicon.caseSensitive, + })); + toggleSection.appendChild(toggle1.domNode); + + const toggle2 = disposableStore.add(new Toggle({ + ...themedToggleStyles, + title: 'Whole Word', + isChecked: true, + icon: Codicon.wholeWord, + })); + toggleSection.appendChild(toggle2.domNode); + + const toggle3 = disposableStore.add(new Toggle({ + ...themedToggleStyles, + title: 'Use Regular Expression', + isChecked: false, + icon: Codicon.regex, + })); + toggleSection.appendChild(toggle3.domNode); + + // Checkboxes + const checkboxSection = $('div'); + checkboxSection.style.display = 'flex'; + checkboxSection.style.flexDirection = 'column'; + checkboxSection.style.gap = '8px'; + container.appendChild(checkboxSection); + + const createCheckboxRow = (label: string, checked: boolean) => { + const row = $('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + + const checkbox = disposableStore.add(new Checkbox(label, checked, themedCheckboxStyles)); + row.appendChild(checkbox.domNode); + + const labelEl = $('span'); + labelEl.textContent = label; + labelEl.style.color = 'var(--vscode-foreground)'; + row.appendChild(labelEl); + + return row; + }; + + checkboxSection.appendChild(createCheckboxRow('Enable auto-save', true)); + checkboxSection.appendChild(createCheckboxRow('Show line numbers', true)); + checkboxSection.appendChild(createCheckboxRow('Word wrap', false)); + + return container; +} + + +// ============================================================================ +// Input Boxes +// ============================================================================ + +function renderInputBoxes({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '16px'; + container.style.width = '350px'; + + // Normal input + const normalInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'Enter search query...', + inputBoxStyles: themedInputBoxStyles, + })); + + // Input with value + const filledInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'File path', + inputBoxStyles: themedInputBoxStyles, + })); + filledInput.value = '/src/vs/editor/browser'; + + // Input with info validation + const infoInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'Username', + inputBoxStyles: themedInputBoxStyles, + validationOptions: { + validation: (value) => value.length < 3 ? { content: 'Username must be at least 3 characters', type: MessageType.INFO } : null + } + })); + infoInput.value = 'ab'; + infoInput.validate(); + + // Input with warning validation + const warningInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'Password', + inputBoxStyles: themedInputBoxStyles, + validationOptions: { + validation: (value) => value.length < 8 ? { content: 'Password should be at least 8 characters for security', type: MessageType.WARNING } : null + } + })); + warningInput.value = 'pass'; + warningInput.validate(); + + // Input with error validation + const errorInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'Email address', + inputBoxStyles: themedInputBoxStyles, + validationOptions: { + validation: (value) => !value.includes('@') ? { content: 'Please enter a valid email address', type: MessageType.ERROR } : null + } + })); + errorInput.value = 'invalid-email'; + errorInput.validate(); + + return container; +} + + +// ============================================================================ +// Count Badges +// ============================================================================ + +function renderCountBadges({ container }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.gap = '12px'; + container.style.alignItems = 'center'; + + // Various badge counts + const counts = [1, 5, 12, 99, 999]; + + for (const count of counts) { + const badgeContainer = $('div'); + badgeContainer.style.display = 'flex'; + badgeContainer.style.alignItems = 'center'; + badgeContainer.style.gap = '8px'; + + const label = $('span'); + label.textContent = 'Issues'; + label.style.color = 'var(--vscode-foreground)'; + badgeContainer.appendChild(label); + + new CountBadge(badgeContainer, { count }, themedBadgeStyles); + container.appendChild(badgeContainer); + } + + return container; +} + + +// ============================================================================ +// Action Bar +// ============================================================================ + +function renderActionBar({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '16px'; + + // Horizontal action bar + const horizontalLabel = $('div'); + horizontalLabel.textContent = 'Horizontal Actions:'; + horizontalLabel.style.color = 'var(--vscode-foreground)'; + horizontalLabel.style.marginBottom = '4px'; + container.appendChild(horizontalLabel); + + const horizontalContainer = $('div'); + container.appendChild(horizontalContainer); + + const horizontalBar = disposableStore.add(new ActionBar(horizontalContainer, { + ariaLabel: 'Editor Actions', + })); + + horizontalBar.push([ + new Action('editor.action.save', 'Save', ThemeIcon.asClassName(Codicon.save), true, async () => console.log('Save')), + new Action('editor.action.undo', 'Undo', ThemeIcon.asClassName(Codicon.discard), true, async () => console.log('Undo')), + new Action('editor.action.redo', 'Redo', ThemeIcon.asClassName(Codicon.redo), true, async () => console.log('Redo')), + new Separator(), + new Action('editor.action.find', 'Find', ThemeIcon.asClassName(Codicon.search), true, async () => console.log('Find')), + new Action('editor.action.replace', 'Replace', ThemeIcon.asClassName(Codicon.replaceAll), true, async () => console.log('Replace')), + ]); + + // Action bar with disabled items + const mixedLabel = $('div'); + mixedLabel.textContent = 'Mixed States:'; + mixedLabel.style.color = 'var(--vscode-foreground)'; + mixedLabel.style.marginBottom = '4px'; + container.appendChild(mixedLabel); + + const mixedContainer = $('div'); + container.appendChild(mixedContainer); + + const mixedBar = disposableStore.add(new ActionBar(mixedContainer, { + ariaLabel: 'Mixed Actions', + })); + + mixedBar.push([ + new Action('action.enabled', 'Enabled', ThemeIcon.asClassName(Codicon.play), true, async () => { }), + new Action('action.disabled', 'Disabled', ThemeIcon.asClassName(Codicon.debugPause), false, async () => { }), + new Action('action.enabled2', 'Enabled', ThemeIcon.asClassName(Codicon.debugStop), true, async () => { }), + ]); + + return container; +} + + +// ============================================================================ +// Progress Bar +// ============================================================================ + +function renderProgressBars({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '24px'; + container.style.width = '400px'; + + const createSection = (label: string) => { + const section = $('div'); + const labelEl = $('div'); + labelEl.textContent = label; + labelEl.style.color = 'var(--vscode-foreground)'; + labelEl.style.marginBottom = '8px'; + labelEl.style.fontSize = '12px'; + section.appendChild(labelEl); + + // Progress bar container with proper constraints + const barContainer = $('div'); + barContainer.style.position = 'relative'; + barContainer.style.width = '100%'; + barContainer.style.height = '4px'; + barContainer.style.overflow = 'hidden'; + section.appendChild(barContainer); + + container.appendChild(section); + return barContainer; + }; + + // Infinite progress + const infiniteSection = createSection('Infinite Progress (loading...)'); + const infiniteBar = disposableStore.add(new ProgressBar(infiniteSection, themedProgressBarOptions)); + infiniteBar.infinite(); + + // Discrete progress - 30% + const progress30Section = createSection('Discrete Progress - 30%'); + const progress30Bar = disposableStore.add(new ProgressBar(progress30Section, themedProgressBarOptions)); + progress30Bar.total(100); + progress30Bar.worked(30); + + // Discrete progress - 60% + const progress60Section = createSection('Discrete Progress - 60%'); + const progress60Bar = disposableStore.add(new ProgressBar(progress60Section, themedProgressBarOptions)); + progress60Bar.total(100); + progress60Bar.worked(60); + + // Discrete progress - 90% + const progress90Section = createSection('Discrete Progress - 90%'); + const progress90Bar = disposableStore.add(new ProgressBar(progress90Section, themedProgressBarOptions)); + progress90Bar.total(100); + progress90Bar.worked(90); + + // Completed progress + const doneSection = createSection('Completed (100%)'); + const doneBar = disposableStore.add(new ProgressBar(doneSection, themedProgressBarOptions)); + doneBar.total(100); + doneBar.worked(100); + + return container; +} + + +// ============================================================================ +// Highlighted Label +// ============================================================================ + +function renderHighlightedLabels({ container }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '8px'; + container.style.color = 'var(--vscode-foreground)'; + + const createHighlightedLabel = (text: string, highlights: { start: number; end: number }[]) => { + const row = $('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + + const labelContainer = $('div'); + const label = new HighlightedLabel(labelContainer); + label.set(text, highlights); + row.appendChild(labelContainer); + + const queryLabel = $('span'); + queryLabel.style.color = 'var(--vscode-descriptionForeground)'; + queryLabel.style.fontSize = '12px'; + queryLabel.textContent = `(matches highlighted)`; + row.appendChild(queryLabel); + + return row; + }; + + // File search examples + container.appendChild(createHighlightedLabel('codeEditorWidget.ts', [{ start: 0, end: 4 }])); // "code" + container.appendChild(createHighlightedLabel('inlineCompletionsController.ts', [{ start: 6, end: 10 }])); // "Comp" + container.appendChild(createHighlightedLabel('diffEditorViewModel.ts', [{ start: 0, end: 4 }, { start: 10, end: 14 }])); // "diff" and "View" + container.appendChild(createHighlightedLabel('workbenchTestServices.ts', [{ start: 9, end: 13 }])); // "Test" + + return container; +} + + +// ============================================================================ +// Export Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ + Buttons: defineComponentFixture({ + render: renderButtons, + }), + + ButtonBar: defineComponentFixture({ + render: renderButtonBar, + }), + + Toggles: defineComponentFixture({ + render: renderToggles, + }), + + InputBoxes: defineComponentFixture({ + render: renderInputBoxes, + }), + + CountBadges: defineComponentFixture({ + render: renderCountBadges, + }), + + ActionBar: defineComponentFixture({ + render: renderActionBar, + }), + + ProgressBars: defineComponentFixture({ + render: renderProgressBars, + }), + + HighlightedLabels: defineComponentFixture({ + render: renderHighlightedLabels, + }), +}); diff --git a/build/vite/fixtures/editor/codeEditor.fixture.ts b/build/vite/fixtures/editor/codeEditor.fixture.ts new file mode 100644 index 00000000000..bdaaf35bf0f --- /dev/null +++ b/build/vite/fixtures/editor/codeEditor.fixture.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../src/vs/base/common/uri'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../src/vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils'; + +const SAMPLE_CODE = `// Welcome to VS Code +function greet(name: string): string { + return \`Hello, \${name}!\`; +} + +class Counter { + private _count = 0; + + increment(): void { + this._count++; + } + + get count(): number { + return this._count; + } +} + +const counter = new Counter(); +counter.increment(); +console.log(greet('World')); +console.log(\`Count: \${counter.count}\`); +`; + +function renderCodeEditor({ container, disposableStore, theme }: ComponentFixtureContext): HTMLElement { + container.style.width = '600px'; + container.style.height = '400px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + + const model = disposableStore.add(createTextModel( + instantiationService, + SAMPLE_CODE, + URI.parse('inmemory://sample.ts'), + 'typescript' + )); + + const editorOptions: ICodeEditorWidgetOptions = { + contributions: [] + }; + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: true }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + fontFamily: 'Consolas, "Courier New", monospace', + renderWhitespace: 'selection', + bracketPairColorization: { enabled: true }, + }, + editorOptions + )); + + editor.setModel(model); + + return container; +} + +export default defineThemedFixtureGroup({ + CodeEditor: defineComponentFixture({ + render: (context) => renderCodeEditor(context), + }), +}); diff --git a/build/vite/fixtures/editor/inlineCompletions.fixture.ts b/build/vite/fixtures/editor/inlineCompletions.fixture.ts new file mode 100644 index 00000000000..06abdece6b6 --- /dev/null +++ b/build/vite/fixtures/editor/inlineCompletions.fixture.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 { constObservable } from '../../../../src/vs/base/common/observable'; +import { URI } from '../../../../src/vs/base/common/uri'; +import { Range } from '../../../../src/vs/editor/common/core/range'; +import { IEditorOptions } from '../../../../src/vs/editor/common/config/editorOptions'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../src/vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { EditorExtensionsRegistry } from '../../../../src/vs/editor/browser/editorExtensions'; +import { InlineCompletionsController } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController'; +import { InlineCompletionsSource, InlineCompletionsState } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource'; +import { InlineEditItem } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem'; +import { TextModelValueReference } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/model/textModelValueReference'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils'; + +// Import to register the inline completions contribution +import '../../../../src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution'; + + +// ============================================================================ +// Inline Edit Fixture +// ============================================================================ + +interface InlineEditOptions extends ComponentFixtureContext { + code: string; + cursorLine: number; + range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number }; + newText: string; + width?: string; + height?: string; + editorOptions?: IEditorOptions; +} + +function renderInlineEdit(options: InlineEditOptions): HTMLElement { + const { container, disposableStore, theme } = options; + container.style.width = options.width ?? '500px'; + container.style.height = options.height ?? '170px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + + const textModel = disposableStore.add(createTextModel( + instantiationService, + options.code, + URI.parse('inmemory://inline-edit.ts'), + 'typescript' + )); + + // Mock the InlineCompletionsSource to provide our test completion + instantiationService.stubInstance(InlineCompletionsSource, { + cancelUpdate: () => { }, + clear: () => { }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + clearOperationOnTextModelChange: constObservable(undefined) as any, + clearSuggestWidgetInlineCompletions: () => { }, + dispose: () => { }, + fetch: async () => true, + inlineCompletions: constObservable(new InlineCompletionsState([ + InlineEditItem.createForTest( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TextModelValueReference.snapshot(textModel as any), + new Range( + options.range.startLineNumber, + options.range.startColumn, + options.range.endLineNumber, + options.range.endColumn + ), + options.newText + ) + ], undefined)), + loading: constObservable(false), + seedInlineCompletionsWithSuggestWidget: () => { }, + seedWithCompletion: () => { }, + suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()), + }); + + const editorWidgetOptions: ICodeEditorWidgetOptions = { + contributions: EditorExtensionsRegistry.getEditorContributions() + }; + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + cursorBlinking: 'solid', + ...options.editorOptions, + }, + editorWidgetOptions + )); + + editor.setModel(textModel); + editor.setPosition({ lineNumber: options.cursorLine, column: 1 }); + editor.focus(); + + // Trigger inline completions + const controller = InlineCompletionsController.get(editor); + controller?.model?.get(); + + return container; +} + + +// ============================================================================ +// Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ + // Side-by-side view: Multi-line replacement + SideBySideView: defineComponentFixture({ + render: (context) => renderInlineEdit({ + ...context, + code: `function greet(name) { + console.log("Hello, " + name); +}`, + cursorLine: 2, + range: { startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 100 }, + newText: ' console.log(`Hello, ${name}!`);', + }), + }), + + // Word replacement view: Single word change + WordReplacementView: defineComponentFixture({ + render: (context) => renderInlineEdit({ + ...context, + code: `class BufferData { + append(data: number[]) { + this.data.push(data); + } +}`, + cursorLine: 2, + range: { startLineNumber: 2, startColumn: 2, endLineNumber: 2, endColumn: 8 }, + newText: 'push', + height: '200px', + }), + }), + + // Insertion view: Insert new content + InsertionView: defineComponentFixture({ + render: (context) => renderInlineEdit({ + ...context, + code: `class BufferData { + append(data: number[]) {} // appends data +}`, + cursorLine: 2, + range: { startLineNumber: 2, startColumn: 26, endLineNumber: 2, endColumn: 26 }, + newText: ` + console.log(data); + `, + height: '200px', + editorOptions: { + inlineSuggest: { + edits: { allowCodeShifting: 'always' } + } + } + }), + }), +}); diff --git a/build/vite/fixtures/fixtureUtils.ts b/build/vite/fixtures/fixtureUtils.ts new file mode 100644 index 00000000000..fd90169dae7 --- /dev/null +++ b/build/vite/fixtures/fixtureUtils.ts @@ -0,0 +1,512 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { defineFixture, defineFixtureGroup, defineFixtureVariants } from '@vscode/component-explorer'; +import { DisposableStore, toDisposable } from '../../../src/vs/base/common/lifecycle'; +import { URI } from '../../../src/vs/base/common/uri'; +import '../style.css'; + +// Theme +import { COLOR_THEME_DARK_INITIAL_COLORS, COLOR_THEME_LIGHT_INITIAL_COLORS } from '../../../src/vs/workbench/services/themes/common/workbenchThemeService'; +import { ColorThemeData } from '../../../src/vs/workbench/services/themes/common/colorThemeData'; +import { ColorScheme } from '../../../src/vs/platform/theme/common/theme'; +import { generateColorThemeCSS } from '../../../src/vs/workbench/services/themes/browser/colorThemeCss'; +import { Registry } from '../../../src/vs/platform/registry/common/platform'; +import { Extensions as ThemingExtensions, IThemingRegistry } from '../../../src/vs/platform/theme/common/themeService'; +import { IEnvironmentService } from '../../../src/vs/platform/environment/common/environment'; +import { getIconsStyleSheet } from '../../../src/vs/platform/theme/browser/iconsStyleSheet'; + +// Instantiation +import { ServiceCollection } from '../../../src/vs/platform/instantiation/common/serviceCollection'; +import { SyncDescriptor } from '../../../src/vs/platform/instantiation/common/descriptors'; +import { ServiceIdentifier } from '../../../src/vs/platform/instantiation/common/instantiation'; +import { TestInstantiationService } from '../../../src/vs/platform/instantiation/test/common/instantiationServiceMock'; + +// Test service implementations +import { TestAccessibilityService } from '../../../src/vs/platform/accessibility/test/common/testAccessibilityService'; +import { MockKeybindingService, MockContextKeyService } from '../../../src/vs/platform/keybinding/test/common/mockKeybindingService'; +import { TestClipboardService } from '../../../src/vs/platform/clipboard/test/common/testClipboardService'; +import { TestEditorWorkerService } from '../../../src/vs/editor/test/common/services/testEditorWorkerService'; +import { NullOpenerService } from '../../../src/vs/platform/opener/test/common/nullOpenerService'; +import { TestNotificationService } from '../../../src/vs/platform/notification/test/common/testNotificationService'; +import { TestDialogService } from '../../../src/vs/platform/dialogs/test/common/testDialogService'; +import { TestConfigurationService } from '../../../src/vs/platform/configuration/test/common/testConfigurationService'; +import { TestTextResourcePropertiesService } from '../../../src/vs/editor/test/common/services/testTextResourcePropertiesService'; +import { TestThemeService } from '../../../src/vs/platform/theme/test/common/testThemeService'; +import { TestLanguageConfigurationService } from '../../../src/vs/editor/test/common/modes/testLanguageConfigurationService'; +import { TestCodeEditorService, TestCommandService } from '../../../src/vs/editor/test/browser/editorTestServices'; +import { TestTreeSitterLibraryService } from '../../../src/vs/editor/test/common/services/testTreeSitterLibraryService'; +import { TestMenuService } from '../../../src/vs/workbench/test/browser/workbenchTestServices'; + +// Service interfaces +import { IAccessibilityService } from '../../../src/vs/platform/accessibility/common/accessibility'; +import { IKeybindingService } from '../../../src/vs/platform/keybinding/common/keybinding'; +import { IClipboardService } from '../../../src/vs/platform/clipboard/common/clipboardService'; +import { IEditorWorkerService } from '../../../src/vs/editor/common/services/editorWorker'; +import { IOpenerService } from '../../../src/vs/platform/opener/common/opener'; +import { INotificationService } from '../../../src/vs/platform/notification/common/notification'; +import { IDialogService } from '../../../src/vs/platform/dialogs/common/dialogs'; +import { IUndoRedoService } from '../../../src/vs/platform/undoRedo/common/undoRedo'; +import { UndoRedoService } from '../../../src/vs/platform/undoRedo/common/undoRedoService'; +import { ILanguageService } from '../../../src/vs/editor/common/languages/language'; +import { LanguageService } from '../../../src/vs/editor/common/services/languageService'; +import { ILanguageConfigurationService } from '../../../src/vs/editor/common/languages/languageConfigurationRegistry'; +import { IConfigurationService } from '../../../src/vs/platform/configuration/common/configuration'; +import { ITextResourcePropertiesService } from '../../../src/vs/editor/common/services/textResourceConfiguration'; +import { IColorTheme, IThemeService } from '../../../src/vs/platform/theme/common/themeService'; +import { ILogService, NullLogService, ILoggerService, NullLoggerService } from '../../../src/vs/platform/log/common/log'; +import { IModelService } from '../../../src/vs/editor/common/services/model'; +import { ModelService } from '../../../src/vs/editor/common/services/modelService'; +import { ICodeEditorService } from '../../../src/vs/editor/browser/services/codeEditorService'; +import { IContextKeyService } from '../../../src/vs/platform/contextkey/common/contextkey'; +import { ICommandService } from '../../../src/vs/platform/commands/common/commands'; +import { ITelemetryService } from '../../../src/vs/platform/telemetry/common/telemetry'; +import { NullTelemetryServiceShape } from '../../../src/vs/platform/telemetry/common/telemetryUtils'; +import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from '../../../src/vs/editor/common/services/languageFeatureDebounce'; +import { ILanguageFeaturesService } from '../../../src/vs/editor/common/services/languageFeatures'; +import { LanguageFeaturesService } from '../../../src/vs/editor/common/services/languageFeaturesService'; +import { ITreeSitterLibraryService } from '../../../src/vs/editor/common/services/treeSitter/treeSitterLibraryService'; +import { IInlineCompletionsService, InlineCompletionsService } from '../../../src/vs/editor/browser/services/inlineCompletionsService'; +import { ICodeLensCache } from '../../../src/vs/editor/contrib/codelens/browser/codeLensCache'; +import { IHoverService } from '../../../src/vs/platform/hover/browser/hover'; +import { IDataChannelService, NullDataChannelService } from '../../../src/vs/platform/dataChannel/common/dataChannel'; +import { IContextMenuService, IContextViewService } from '../../../src/vs/platform/contextview/browser/contextView'; +import { ILabelService } from '../../../src/vs/platform/label/common/label'; +import { IMenuService } from '../../../src/vs/platform/actions/common/actions'; +import { IActionViewItemService, NullActionViewItemService } from '../../../src/vs/platform/actions/browser/actionViewItemService'; +import { IDefaultAccountService } from '../../../src/vs/platform/defaultAccount/common/defaultAccount'; +import { IStorageService, IStorageValueChangeEvent, IWillSaveStateEvent, StorageScope, StorageTarget, IStorageTargetChangeEvent, IStorageEntry, WillSaveStateReason, IWorkspaceStorageValueChangeEvent, IProfileStorageValueChangeEvent, IApplicationStorageValueChangeEvent } from '../../../src/vs/platform/storage/common/storage'; +import { Emitter, Event } from '../../../src/vs/base/common/event'; +import { mock } from '../../../src/vs/base/test/common/mock'; +import { IAnyWorkspaceIdentifier } from '../../../src/vs/platform/workspace/common/workspace'; +import { IUserDataProfile } from '../../../src/vs/platform/userDataProfile/common/userDataProfile'; +import { IUserInteractionService, MockUserInteractionService } from '../../../src/vs/platform/userInteraction/browser/userInteractionService'; + +// Editor +import { ITextModel } from '../../../src/vs/editor/common/model'; + + + +// Import color registrations to ensure colors are available +import '../../../src/vs/platform/theme/common/colors/baseColors'; +import '../../../src/vs/platform/theme/common/colors/editorColors'; +import '../../../src/vs/platform/theme/common/colors/listColors'; +import '../../../src/vs/platform/theme/common/colors/miscColors'; +import '../../../src/vs/workbench/common/theme'; + +/** + * A storage service that never stores anything and always returns the default/fallback value. + * This is useful for fixtures where we want consistent behavior without persisted state. + */ +class NullStorageService implements IStorageService { + + declare readonly _serviceBrand: undefined; + + private readonly _onDidChangeValue = new Emitter(); + onDidChangeValue(scope: StorageScope.WORKSPACE, key: string | undefined, disposable: DisposableStore): Event; + onDidChangeValue(scope: StorageScope.PROFILE, key: string | undefined, disposable: DisposableStore): Event; + onDidChangeValue(scope: StorageScope.APPLICATION, key: string | undefined, disposable: DisposableStore): Event; + onDidChangeValue(scope: StorageScope, key: string | undefined, disposable: DisposableStore): Event { + return Event.filter(this._onDidChangeValue.event, e => e.scope === scope && (key === undefined || e.key === key), disposable); + } + + private readonly _onDidChangeTarget = new Emitter(); + readonly onDidChangeTarget: Event = this._onDidChangeTarget.event; + + private readonly _onWillSaveState = new Emitter(); + readonly onWillSaveState: Event = this._onWillSaveState.event; + + get(key: string, scope: StorageScope, fallbackValue: string): string; + get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined; + get(_key: string, _scope: StorageScope, fallbackValue?: string): string | undefined { + return fallbackValue; + } + + getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean; + getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined; + getBoolean(_key: string, _scope: StorageScope, fallbackValue?: boolean): boolean | undefined { + return fallbackValue; + } + + getNumber(key: string, scope: StorageScope, fallbackValue: number): number; + getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined; + getNumber(_key: string, _scope: StorageScope, fallbackValue?: number): number | undefined { + return fallbackValue; + } + + getObject(key: string, scope: StorageScope, fallbackValue: T): T; + getObject(key: string, scope: StorageScope, fallbackValue?: T): T | undefined; + getObject(_key: string, _scope: StorageScope, fallbackValue?: T): T | undefined { + return fallbackValue; + } + + store(_key: string, _value: string | boolean | number | undefined | null, _scope: StorageScope, _target: StorageTarget): void { + // no-op + } + + storeAll(_entries: IStorageEntry[], _external: boolean): void { + // no-op + } + + remove(_key: string, _scope: StorageScope): void { + // no-op + } + + isNew(_scope: StorageScope): boolean { + return true; + } + + flush(_reason?: WillSaveStateReason): Promise { + return Promise.resolve(); + } + + optimize(_scope: StorageScope): Promise { + return Promise.resolve(); + } + + log(): void { + // no-op + } + + keys(_scope: StorageScope, _target: StorageTarget): string[] { + return []; + } + + switch(): Promise { + return Promise.resolve(); + } + + hasScope(_scope: IAnyWorkspaceIdentifier | IUserDataProfile): boolean { + return false; + } +} + + +// ============================================================================ +// Themes +// ============================================================================ + +const themingRegistry = Registry.as(ThemingExtensions.ThemingContribution); +const mockEnvironmentService: IEnvironmentService = Object.create(null); + +export const darkTheme = ColorThemeData.createUnloadedThemeForThemeType( + ColorScheme.DARK, + COLOR_THEME_DARK_INITIAL_COLORS +); + +export const lightTheme = ColorThemeData.createUnloadedThemeForThemeType( + ColorScheme.LIGHT, + COLOR_THEME_LIGHT_INITIAL_COLORS +); + +let globalStyleSheet: CSSStyleSheet | undefined; +let iconsStyleSheetCache: CSSStyleSheet | undefined; +let darkThemeStyleSheet: CSSStyleSheet | undefined; +let lightThemeStyleSheet: CSSStyleSheet | undefined; + +function getGlobalStyleSheet(): CSSStyleSheet { + if (!globalStyleSheet) { + globalStyleSheet = new CSSStyleSheet(); + const globalRules: string[] = []; + for (const sheet of Array.from(document.styleSheets)) { + try { + for (const rule of Array.from(sheet.cssRules)) { + globalRules.push(rule.cssText); + } + } catch { + // Cross-origin stylesheets can't be read + } + } + globalStyleSheet.replaceSync(globalRules.join('\n')); + } + return globalStyleSheet; +} + +function getIconsStyleSheetCached(): CSSStyleSheet { + if (!iconsStyleSheetCache) { + iconsStyleSheetCache = new CSSStyleSheet(); + const iconsSheet = getIconsStyleSheet(undefined); + iconsStyleSheetCache.replaceSync(iconsSheet.getCSS() as string); + iconsSheet.dispose(); + } + return iconsStyleSheetCache; +} + +function getThemeStyleSheet(theme: ColorThemeData): CSSStyleSheet { + const isDark = theme.type === ColorScheme.DARK; + if (isDark && darkThemeStyleSheet) { + return darkThemeStyleSheet; + } + if (!isDark && lightThemeStyleSheet) { + return lightThemeStyleSheet; + } + + const sheet = new CSSStyleSheet(); + const css = generateColorThemeCSS( + theme, + ':host', + themingRegistry.getThemingParticipants(), + mockEnvironmentService + ); + sheet.replaceSync(css.code); + + if (isDark) { + darkThemeStyleSheet = sheet; + } else { + lightThemeStyleSheet = sheet; + } + return sheet; +} + +/** + * Applies theme styling to a shadow DOM container. + * Adds theme class names and adopts shared stylesheets. + */ +export function setupTheme(container: HTMLElement, theme: ColorThemeData): void { + container.classList.add(...theme.classNames); + + const shadowRoot = container.getRootNode() as ShadowRoot; + if (shadowRoot.adoptedStyleSheets !== undefined) { + shadowRoot.adoptedStyleSheets = [ + getGlobalStyleSheet(), + getIconsStyleSheetCached(), + getThemeStyleSheet(theme), + ]; + } +} + + +// ============================================================================ +// Services +// ============================================================================ + +export interface ServiceRegistration { + define(id: ServiceIdentifier, ctor: new (...args: never[]) => T): void; + defineInstance(id: ServiceIdentifier, instance: T): void; +} + +export interface CreateServicesOptions { + /** + * The color theme to use for the theme service. + */ + colorTheme?: IColorTheme; + /** + * Additional services to register after the base editor services. + */ + additionalServices?: (registration: ServiceRegistration) => void; +} + +/** + * Creates a TestInstantiationService with all services needed for CodeEditorWidget. + * Additional services can be registered via the options callback. + */ +export function createEditorServices(disposables: DisposableStore, options?: CreateServicesOptions): TestInstantiationService { + const services = new ServiceCollection(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const serviceIdentifiers: ServiceIdentifier[] = []; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const define = (id: ServiceIdentifier, ctor: new (...args: any[]) => T) => { + if (!services.has(id)) { + services.set(id, new SyncDescriptor(ctor)); + } + serviceIdentifiers.push(id); + }; + + const defineInstance = (id: ServiceIdentifier, instance: T) => { + if (!services.has(id)) { + services.set(id, instance); + } + serviceIdentifiers.push(id); + }; + + // Base editor services + define(IAccessibilityService, TestAccessibilityService); + define(IKeybindingService, MockKeybindingService); + define(IClipboardService, TestClipboardService); + define(IEditorWorkerService, TestEditorWorkerService); + defineInstance(IOpenerService, NullOpenerService); + define(INotificationService, TestNotificationService); + define(IDialogService, TestDialogService); + define(IUndoRedoService, UndoRedoService); + define(ILanguageService, LanguageService); + define(ILanguageConfigurationService, TestLanguageConfigurationService); + define(IConfigurationService, TestConfigurationService); + define(ITextResourcePropertiesService, TestTextResourcePropertiesService); + defineInstance(IStorageService, new NullStorageService()); + if (options?.colorTheme) { + defineInstance(IThemeService, new TestThemeService(options.colorTheme)); + } else { + define(IThemeService, TestThemeService); + } + define(ILogService, NullLogService); + define(IModelService, ModelService); + define(ICodeEditorService, TestCodeEditorService); + define(IContextKeyService, MockContextKeyService); + define(ICommandService, TestCommandService); + define(ITelemetryService, NullTelemetryServiceShape); + define(ILoggerService, NullLoggerService); + define(IDataChannelService, NullDataChannelService); + define(IEnvironmentService, class extends mock() { + declare readonly _serviceBrand: undefined; + override isBuilt: boolean = true; + override isExtensionDevelopment: boolean = false; + }); + define(ILanguageFeatureDebounceService, LanguageFeatureDebounceService); + define(ILanguageFeaturesService, LanguageFeaturesService); + define(ITreeSitterLibraryService, TestTreeSitterLibraryService); + define(IInlineCompletionsService, InlineCompletionsService); + defineInstance(ICodeLensCache, { + _serviceBrand: undefined, + put: () => { }, + get: () => undefined, + delete: () => { }, + } as ICodeLensCache); + defineInstance(IHoverService, { + _serviceBrand: undefined, + showDelayedHover: () => undefined, + setupDelayedHover: () => ({ dispose: () => { } }), + setupDelayedHoverAtMouse: () => ({ dispose: () => { } }), + showInstantHover: () => undefined, + hideHover: () => { }, + showAndFocusLastHover: () => { }, + setupManagedHover: () => ({ dispose: () => { }, show: () => { }, hide: () => { }, update: () => { } }), + showManagedHover: () => { }, + } as IHoverService); + defineInstance(IDefaultAccountService, { + _serviceBrand: undefined, + onDidChangeDefaultAccount: new Emitter().event, + onDidChangePolicyData: new Emitter().event, + policyData: null, + getDefaultAccount: async () => null, + getDefaultAccountAuthenticationProvider: () => ({ id: 'test', name: 'Test', scopes: [], enterprise: false }), + setDefaultAccountProvider: () => { }, + refresh: async () => null, + signIn: async () => null, + } as IDefaultAccountService); + + // User interaction service with focus simulation enabled (all elements appear focused in fixtures) + defineInstance(IUserInteractionService, new MockUserInteractionService(true, false)); + + // Allow additional services to be registered + options?.additionalServices?.({ define, defineInstance }); + + const instantiationService = disposables.add(new TestInstantiationService(services, true)); + + disposables.add(toDisposable(() => { + for (const id of serviceIdentifiers) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const instanceOrDescriptor = services.get(id) as any; + if (typeof instanceOrDescriptor?.dispose === 'function') { + instanceOrDescriptor.dispose(); + } + } + })); + + return instantiationService; +} + +/** + * Registers additional services needed by workbench components (merge editor, etc.). + * Use with createEditorServices additionalServices option. + */ +export function registerWorkbenchServices(registration: ServiceRegistration): void { + registration.defineInstance(IContextMenuService, { + showContextMenu: () => { }, + onDidShowContextMenu: () => ({ dispose: () => { } }), + onDidHideContextMenu: () => ({ dispose: () => { } }), + } as unknown as IContextMenuService); + + registration.defineInstance(IContextViewService, { + showContextView: () => ({ dispose: () => { } }), + hideContextView: () => { }, + getContextViewElement: () => null, + layout: () => { }, + } as unknown as IContextViewService); + + registration.defineInstance(ILabelService, { + getUriLabel: (uri: URI) => uri.path, + getUriBasenameLabel: (uri: URI) => uri.path.split('/').pop() ?? '', + getWorkspaceLabel: () => '', + getHostLabel: () => '', + getSeparator: () => '/', + registerFormatter: () => ({ dispose: () => { } }), + onDidChangeFormatters: () => ({ dispose: () => { } }), + registerCachedFormatter: () => ({ dispose: () => { } }), + } as unknown as ILabelService); + + registration.define(IMenuService, TestMenuService); + registration.define(IActionViewItemService, NullActionViewItemService); +} + + +// ============================================================================ +// Text Models +// ============================================================================ + +/** + * Creates a text model using the ModelService. + */ +export function createTextModel( + instantiationService: TestInstantiationService, + text: string, + uri: URI, + languageId?: string +): ITextModel { + const modelService = instantiationService.get(IModelService); + const languageService = instantiationService.get(ILanguageService); + const languageSelection = languageId ? languageService.createById(languageId) : null; + return modelService.createModel(text, languageSelection, uri); +} + + +// ============================================================================ +// Fixture Adapters +// ============================================================================ + +export interface ComponentFixtureContext { + container: HTMLElement; + disposableStore: DisposableStore; + theme: ColorThemeData; +} + +export interface ComponentFixtureOptions { + render: (context: ComponentFixtureContext) => HTMLElement | Promise; +} + +type ThemedFixtures = ReturnType; + +/** + * Creates Dark and Light fixture variants from a single render function. + * The render function receives a context with container and disposableStore. + */ +export function defineComponentFixture(options: ComponentFixtureOptions): ThemedFixtures { + const createFixture = (theme: typeof darkTheme | typeof lightTheme) => defineFixture({ + isolation: 'shadow-dom', + displayMode: { type: 'component' }, + properties: [], + background: theme === darkTheme ? 'dark' : 'light', + render: async (container: HTMLElement) => { + const disposableStore = new DisposableStore(); + setupTheme(container, theme); + return options.render({ container, disposableStore, theme }); + }, + }); + + return defineFixtureVariants({ + Dark: createFixture(darkTheme), + Light: createFixture(lightTheme), + }); +} + +type ThemedFixtureGroupInput = Record; + +/** + * Creates a nested fixture group from themed fixtures. + * E.g., { MergeEditor: { Dark: ..., Light: ... } } becomes a nested group: MergeEditor > Dark/Light + */ +export function defineThemedFixtureGroup(group: ThemedFixtureGroupInput): ReturnType { + return defineFixtureGroup(group); +} diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index 97fb9dc76aa..4db5338149d 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -8,62 +8,87 @@ "name": "@vscode/sample-source", "version": "0.0.0", "devDependencies": { - "@vscode/rollup-plugin-esm-url": "^1.0.1-0", - "vite": "^7.1.11" + "@vscode/component-explorer": "next", + "@vscode/component-explorer-vite-plugin": "next", + "@vscode/rollup-plugin-esm-url": "^1.0.1-1", + "vite": "npm:rolldown-vite@latest" } }, - "../lib": { - "name": "monaco-editor-core", - "version": "0.0.0", - "extraneous": true, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, "license": "MIT", + "optional": true, "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" + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" } }, - "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" - ], + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "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" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "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==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.101.0.tgz", + "integrity": "sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.101.0.tgz", + "integrity": "sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==", "cpu": [ "arm64" ], @@ -74,30 +99,13 @@ "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==", "cpu": [ "arm64" ], @@ -108,13 +116,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==", "cpu": [ "x64" ], @@ -125,30 +133,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==", "cpu": [ "x64" ], @@ -159,13 +150,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.53.tgz", + "integrity": "sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==", "cpu": [ "arm" ], @@ -176,13 +167,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==", "cpu": [ "arm64" ], @@ -193,132 +184,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==", "cpu": [ "arm64" ], @@ -326,16 +198,16 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==", "cpu": [ "x64" ], @@ -343,33 +215,16 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==", "cpu": [ "x64" ], @@ -377,16 +232,16 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==", "cpu": [ "arm64" ], @@ -397,30 +252,30 @@ "openharmony" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.53.tgz", + "integrity": "sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==", "cpu": [ - "x64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.0" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "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==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==", "cpu": [ "arm64" ], @@ -431,30 +286,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==", "cpu": [ "x64" ], @@ -465,13 +303,20 @@ "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -480,12 +325,13 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -494,12 +340,13 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -508,12 +355,13 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -522,12 +370,13 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -536,12 +385,13 @@ "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -550,12 +400,13 @@ "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -564,12 +415,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -578,12 +430,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -592,12 +445,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -606,12 +460,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, - "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==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", "cpu": [ "loong64" ], @@ -620,12 +475,28 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", "cpu": [ "ppc64" ], @@ -634,12 +505,28 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -648,12 +535,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -662,12 +550,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -676,12 +565,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], @@ -690,12 +580,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -704,12 +595,43 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -718,12 +640,13 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "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==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -732,12 +655,13 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, - "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==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "cpu": [ "x64" ], @@ -746,65 +670,84 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } }, "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" + "license": "MIT", + "peer": true + }, + "node_modules/@vscode/component-explorer": { + "version": "0.1.1-2", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-2.tgz", + "integrity": "sha512-2VMoXLnDBk+hKrhw+iGUsEjnCd1YiiZqe+1LdQIKdk16zqYRtJ5iO6yDxZ4cKy3Wphd+qLDUWmZSULNtKioMrQ==", + "dev": true, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@vscode/component-explorer-vite-plugin": { + "version": "0.1.1-2", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-2.tgz", + "integrity": "sha512-iYSp8shDZEJJrjMWGneWyjFbFyED5Og74c9h5XBmVPZBDN4INfOTmPlC+HYTv/CL5+NFxpl91CdtacCmqz2EXw==", + "dev": true, + "dependencies": { + "tinyglobby": "^0.2.0" + }, + "peerDependencies": { + "@vscode/component-explorer": "*", + "vite": "*" + } }, "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==", + "version": "1.0.1-1", + "resolved": "https://registry.npmjs.org/@vscode/rollup-plugin-esm-url/-/rollup-plugin-esm-url-1.0.1-1.tgz", + "integrity": "sha512-vNmIR3ZyiwACUi8qnXhKNukoXaFkOM9skiqVOVHNKJTBb7kJS+evtyadrBc/fMm1y303WQWBNA90E7fCCsE2Sw==", "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==", + "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==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "license": "Apache-2.0", "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": ">=8" } }, "node_modules/fdir": { @@ -840,6 +783,289 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.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==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -908,12 +1134,74 @@ "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==", + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.53.tgz", + "integrity": "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.101.0", + "@rolldown/pluginutils": "1.0.0-beta.53" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-x64": "1.0.0-beta.53", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.53", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.53", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.53", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.53", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.53", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.53" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -925,29 +1213,45 @@ "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", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -975,18 +1279,28 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "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", + "optional": true + }, "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==", + "name": "rolldown-vite", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.3.1.tgz", + "integrity": "sha512-LYzdNAjRHhF2yA4JUQm/QyARyi216N2rpJ0lJZb8E9FU2y5v6Vk+xq/U4XBOxMefpWixT5H3TslmAHm1rqIq2w==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "@oxc-project/runtime": "0.101.0", "fdir": "^6.5.0", + "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rollup": "^4.43.0", + "rolldown": "1.0.0-beta.53", "tinyglobby": "^0.2.15" }, "bin": { @@ -1003,9 +1317,9 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "esbuild": "^0.27.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", @@ -1018,15 +1332,15 @@ "@types/node": { "optional": true }, + "esbuild": { + "optional": true + }, "jiti": { "optional": true }, "less": { "optional": true }, - "lightningcss": { - "optional": true - }, "sass": { "optional": true }, diff --git a/build/vite/package.json b/build/vite/package.json index f65f359145e..9800e54af40 100644 --- a/build/vite/package.json +++ b/build/vite/package.json @@ -9,7 +9,14 @@ "preview": "vite preview" }, "devDependencies": { - "@vscode/rollup-plugin-esm-url": "^1.0.1-0", - "vite": "^7.1.11" + "@vscode/rollup-plugin-esm-url": "^1.0.1-1", + "vite": "npm:rolldown-vite@latest", + "@vscode/component-explorer": "next", + "@vscode/component-explorer-vite-plugin": "next" + }, + "overrides": { + "@vscode/component-explorer-vite-plugin": { + "vite": "$vite" + } } } diff --git a/build/vite/setup-dev.ts b/build/vite/setup-dev.ts index 8f1f997599c..b0ef8682c9a 100644 --- a/build/vite/setup-dev.ts +++ b/build/vite/setup-dev.ts @@ -10,6 +10,7 @@ import { getSingletonServiceDescriptors, InstantiationType, registerSingleton } 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'; +import './style.css'; enableHotReload(); diff --git a/build/vite/style.css b/build/vite/style.css index b9573061e51..f1ee3ca812f 100644 --- a/build/vite/style.css +++ b/build/vite/style.css @@ -7,3 +7,9 @@ height: 400px; border: 1px solid black; } + +@font-face { + font-family: "codicon"; + font-display: block; + src: url("~@vscode/codicons/dist/codicon.ttf") format("truetype"); +} diff --git a/build/vite/vite.config.ts b/build/vite/vite.config.ts index 1ddb4f53adf..6cdde88076a 100644 --- a/build/vite/vite.config.ts +++ b/build/vite/vite.config.ts @@ -5,9 +5,10 @@ import { createLogger, defineConfig, Plugin } from 'vite'; import path, { join } from 'path'; -import { rollupEsmUrlPlugin } from '@vscode/rollup-plugin-esm-url'; +import { componentExplorer } from '@vscode/component-explorer-vite-plugin'; import { statSync } from 'fs'; import { pathToFileURL } from 'url'; +import { rollupEsmUrlPlugin } from '@vscode/rollup-plugin-esm-url'; function injectBuiltinExtensionsPlugin(): Plugin { let builtinExtensionsCache: unknown[] | null = null; @@ -166,9 +167,18 @@ export default defineConfig({ plugins: [ rollupEsmUrlPlugin({}), injectBuiltinExtensionsPlugin(), - createHotClassSupport() + createHotClassSupport(), + componentExplorer({ + logLevel: 'verbose', + include: 'build/vite/**/*.fixture.ts', + }), ], customLogger: logger, + resolve: { + alias: { + '~@vscode/codicons': '/node_modules/@vscode/codicons', + } + }, esbuild: { tsconfigRaw: { compilerOptions: { diff --git a/extensions/json/package.json b/extensions/json/package.json index 73265dc5f23..1bc6fa85e53 100644 --- a/extensions/json/package.json +++ b/extensions/json/package.json @@ -70,6 +70,9 @@ ".ember-cli", "typedoc.json" ], + "filenamePatterns": [ + "**/.github/hooks/*.json" + ], "configuration": "./language-configuration.json" }, { diff --git a/package-lock.json b/package-lock.json index 7c6806f2b4d..47f013803d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-5", + "@vscode/codicons": "^0.0.45-6", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -2947,9 +2947,9 @@ ] }, "node_modules/@vscode/codicons": { - "version": "0.0.45-5", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-5.tgz", - "integrity": "sha512-tvRtMrVAe+CnlePF1Z3uhRfu0mLhAVgrSiY9zuEaGyXyBlN1/06bnpXno8XYb4O3ggOFx2phximqje4dhoLnkQ==", + "version": "0.0.45-6", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-6.tgz", + "integrity": "sha512-HjJmIxw6anUPk/yiQTyF60ERjARNfc/A11kKoiO7jg2bzNeaCexunu4oUo/W8lHGr/dvHxYcruM1V3ZoGxyFNQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/deviceid": { diff --git a/package.json b/package.json index e79cfc22656..545b1a0812d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "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-all2 -lp watch-client watch-extensions", + "watch": "npm-run-all2 -lp watch-client-transpile watch-client watch-extensions", "watchd": "deemon npm run watch", "watch-webd": "deemon npm run watch-web", "kill-watchd": "deemon --kill npm run watch", @@ -30,6 +30,9 @@ "watch-client": "npm run gulp watch-client", "watch-clientd": "deemon npm run watch-client", "kill-watch-clientd": "deemon --kill npm run watch-client", + "watch-client-transpile": "npx tsx build/next/index.ts transpile --watch", + "watch-client-transpiled": "deemon npm run watch-client-transpile", + "kill-watch-client-transpiled": "deemon --kill npm run watch-client-transpile", "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", @@ -77,7 +80,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-5", + "@vscode/codicons": "^0.0.45-6", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index e83d2319329..48c97fba4c5 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-5", + "@vscode/codicons": "^0.0.45-6", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.45-5", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-5.tgz", - "integrity": "sha512-tvRtMrVAe+CnlePF1Z3uhRfu0mLhAVgrSiY9zuEaGyXyBlN1/06bnpXno8XYb4O3ggOFx2phximqje4dhoLnkQ==", + "version": "0.0.45-6", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-6.tgz", + "integrity": "sha512-HjJmIxw6anUPk/yiQTyF60ERjARNfc/A11kKoiO7jg2bzNeaCexunu4oUo/W8lHGr/dvHxYcruM1V3ZoGxyFNQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index f738d5554fa..4f591ed9906 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-5", + "@vscode/codicons": "^0.0.45-6", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.css b/src/vs/base/browser/ui/codicons/codicon/codicon.css index 02154e77b68..d7f257db934 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon.css +++ b/src/vs/base/browser/ui/codicons/codicon/codicon.css @@ -6,7 +6,7 @@ @font-face { font-family: "codicon"; font-display: block; - src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype"); + src: url("./codicon.ttf") format("truetype"); } .codicon[class*='codicon-'] { diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index a3098e1715c..ff46f210b8a 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index f39515c3348..5195e0b6353 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1182,6 +1182,13 @@ export interface ITextModel { */ getCustomLineHeightsDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain custom line heights. + * @param range The range to search in + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getCustomLineHeightsDecorationsInRange(range: Range, ownerId?: number): IModelDecoration[]; + /** * @internal */ diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index ebd3eccec51..bb3a86c9857 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1563,6 +1563,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati rawContentChanges.push( new ModelRawLineChanged( editLineNumber, + currentEditLineNumber, this.getLineContent(currentEditLineNumber), decorationsInCurrentLine )); @@ -1593,7 +1594,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati rawContentChanges.push( new ModelRawLinesInserted( spliceLineNumber + 1, - startLineNumber + insertingLinesCnt, + fromLineNumber, + cnt, newLines, injectedTexts ) @@ -1653,7 +1655,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (affectedInjectedTextLines && affectedInjectedTextLines.size > 0) { const affectedLines = Array.from(affectedInjectedTextLines); - const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); + const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); this._onDidChangeContentOrInjectedText(new ModelInjectedTextChangedEvent(lineChangeEvents)); } this._fireOnDidChangeLineHeight(affectedLineHeights); @@ -1865,6 +1867,12 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return decs; } + public getCustomLineHeightsDecorationsInRange(range: Range, ownerId: number = 0): model.IModelDecoration[] { + const decs = this._decorationsTree.getCustomLineHeightsInInterval(this, this.getOffsetAt(range.getStartPosition()), this.getOffsetAt(range.getEndPosition()), ownerId); + pushMany(decs, this._fontTokenDecorationsProvider.getDecorationsInRange(range, ownerId)); + return decs; + } + private _getInjectedTextInLine(lineNumber: number): LineInjectedText[] { const startOffset = this._buffer.getOffsetAt(lineNumber, 1); const endOffset = startOffset + this._buffer.getLineLength(lineNumber); @@ -2249,6 +2257,12 @@ class DecorationsTrees { return this._ensureNodesHaveRanges(host, result).filter((i) => typeof i.options.lineHeight === 'number'); } + public getCustomLineHeightsInInterval(host: IDecorationsTreesHost, start: number, end: number, filterOwnerId: number): model.IModelDecoration[] { + const versionId = host.getVersionId(); + const result = this._intervalSearch(start, end, filterOwnerId, false, false, versionId, false); + return this._ensureNodesHaveRanges(host, result).filter((i) => typeof i.options.lineHeight === 'number'); + } + public getAll(host: IDecorationsTreesHost, filterOwnerId: number, filterOutValidation: boolean, filterFontDecorations: boolean, overviewRulerOnly: boolean, onlyMarginDecorations: boolean): model.IModelDecoration[] { const versionId = host.getVersionId(); const result = this._search(filterOwnerId, filterOutValidation, filterFontDecorations, overviewRulerOnly, versionId, onlyMarginDecorations); diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index b25c00aae8a..4fa24afd2c1 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -308,9 +308,13 @@ export class LineInjectedText { export class ModelRawLineChanged { public readonly changeType = RawContentChangedType.LineChanged; /** - * The line that has changed. + * The line number that has changed (before the change was applied). */ public readonly lineNumber: number; + /** + * The new line number the old one is mapped to (after the change was applied). + */ + public readonly lineNumberPostEdit: number; /** * The new value of the line. */ @@ -320,8 +324,9 @@ export class ModelRawLineChanged { */ public readonly injectedText: LineInjectedText[] | null; - constructor(lineNumber: number, detail: string, injectedText: LineInjectedText[] | null) { + constructor(lineNumber: number, lineNumberPostEdit: number, detail: string, injectedText: LineInjectedText[] | null) { this.lineNumber = lineNumber; + this.lineNumberPostEdit = lineNumberPostEdit; this.detail = detail; this.injectedText = injectedText; } @@ -409,10 +414,26 @@ export class ModelRawLinesInserted { * Before what line did the insertion begin */ public readonly fromLineNumber: number; + /** + * The actual start line number in the updated buffer where the newly inserted content can be found. + */ + public readonly fromLineNumberPostEdit: number; + /** + * The count of inserted lines. + */ + public readonly count: number; /** * `toLineNumber` - `fromLineNumber` + 1 denotes the number of lines that were inserted */ - public readonly toLineNumber: number; + public get toLineNumber(): number { + return this.fromLineNumber + this.count - 1; + } + /** + * The actual end line number of the insertion in the updated buffer. + */ + public get toLineNumberPostEdit(): number { + return this.fromLineNumberPostEdit + this.count - 1; + } /** * The text that was inserted */ @@ -422,10 +443,11 @@ export class ModelRawLinesInserted { */ public readonly injectedTexts: (LineInjectedText[] | null)[]; - constructor(fromLineNumber: number, toLineNumber: number, detail: string[], injectedTexts: (LineInjectedText[] | null)[]) { + constructor(fromLineNumber: number, fromLineNumberPostEdit: number, count: number, detail: string[], injectedTexts: (LineInjectedText[] | null)[]) { this.injectedTexts = injectedTexts; this.fromLineNumber = fromLineNumber; - this.toLineNumber = toLineNumber; + this.fromLineNumberPostEdit = fromLineNumberPostEdit; + this.count = count; this.detail = detail; } } diff --git a/src/vs/editor/common/viewLayout/lineHeights.ts b/src/vs/editor/common/viewLayout/lineHeights.ts index 47e402e075b..bceebf4aad9 100644 --- a/src/vs/editor/common/viewLayout/lineHeights.ts +++ b/src/vs/editor/common/viewLayout/lineHeights.ts @@ -5,6 +5,10 @@ import { binarySearch2 } from '../../../base/common/arrays.js'; import { intersection } from '../../../base/common/collections.js'; +import { IEditorConfiguration } from '../config/editorConfiguration.js'; +import { EditorOption } from '../config/editorOptions.js'; +import { ICoordinatesConverter } from '../coordinatesConverter.js'; +import { IModelDecoration } from '../model.js'; export class CustomLine { @@ -62,7 +66,7 @@ export class LineHeightsManager { private _defaultLineHeight: number; private _hasPending: boolean = false; - constructor(defaultLineHeight: number, customLineHeightData: ICustomLineHeightData[]) { + constructor(defaultLineHeight: number, customLineHeightData: CustomLineHeightData[]) { this._defaultLineHeight = defaultLineHeight; if (customLineHeightData.length > 0) { for (const data of customLineHeightData) { @@ -227,7 +231,7 @@ export class LineHeightsManager { } } - public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { + public onLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[]): void { const insertCount = toLineNumber - fromLineNumber + 1; const candidateStartIndexOfInsertion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber); let startIndexOfInsertion: number; @@ -243,7 +247,22 @@ export class LineHeightsManager { } else { startIndexOfInsertion = -(candidateStartIndexOfInsertion + 1); } - const toReAdd: ICustomLineHeightData[] = []; + const maxLineHeightPerLine = new Map(); + for (const lineHeightAdded of lineHeightsAdded) { + for (let lineNumber = lineHeightAdded.startLineNumber; lineNumber <= lineHeightAdded.endLineNumber; lineNumber++) { + if (lineNumber >= fromLineNumber && lineNumber <= toLineNumber) { + const currentMax = maxLineHeightPerLine.get(lineNumber) ?? this._defaultLineHeight; + maxLineHeightPerLine.set(lineNumber, Math.max(currentMax, lineHeightAdded.lineHeight)); + } + } + this.insertOrChangeCustomLineHeight( + lineHeightAdded.decorationId, + lineHeightAdded.startLineNumber, + lineHeightAdded.endLineNumber, + lineHeightAdded.lineHeight + ); + } + const toReAdd: CustomLineHeightData[] = []; const decorationsImmediatelyAfter = new Set(); for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { if (this._orderedCustomLines[i].lineNumber === fromLineNumber) { @@ -257,9 +276,12 @@ export class LineHeightsManager { } } const decorationsWithGaps = intersection(decorationsImmediatelyBefore, decorationsImmediatelyAfter); + const specialHeightToAdd = Array.from(maxLineHeightPerLine.values()).reduce((acc, height) => acc + height, 0); + const defaultHeightToAdd = (insertCount - maxLineHeightPerLine.size) * this._defaultLineHeight; + const prefixSumToAdd = specialHeightToAdd + defaultHeightToAdd; for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { this._orderedCustomLines[i].lineNumber += insertCount; - this._orderedCustomLines[i].prefixSum += this._defaultLineHeight * insertCount; + this._orderedCustomLines[i].prefixSum += prefixSumToAdd; } if (decorationsWithGaps.size > 0) { @@ -281,8 +303,8 @@ export class LineHeightsManager { for (const dec of toReAdd) { this.insertOrChangeCustomLineHeight(dec.decorationId, dec.startLineNumber, dec.endLineNumber, dec.lineHeight); } - this.commit(); } + this.commit(); } public commit(): void { @@ -363,11 +385,27 @@ export class LineHeightsManager { } } -export interface ICustomLineHeightData { - readonly decorationId: string; - readonly startLineNumber: number; - readonly endLineNumber: number; - readonly lineHeight: number; +export class CustomLineHeightData { + + constructor( + readonly decorationId: string, + readonly startLineNumber: number, + readonly endLineNumber: number, + readonly lineHeight: number + ) { } + + public static fromDecorations(decorations: IModelDecoration[], coordinatesConverter: ICoordinatesConverter, configuration: IEditorConfiguration): CustomLineHeightData[] { + const defaultLineHeight = configuration.options.get(EditorOption.lineHeight); + return decorations.map((d) => { + const viewRange = coordinatesConverter.convertModelRangeToViewRange(d.range); + return new CustomLineHeightData( + d.id, + viewRange.startLineNumber, + viewRange.endLineNumber, + d.options.lineHeight ? d.options.lineHeight * defaultLineHeight : 0 + ); + }); + } } class ArrayMap { diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index b773e5dd4cb..376b0f2c7c9 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -5,7 +5,7 @@ import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from '../viewModel.js'; import * as strings from '../../../base/common/strings.js'; -import { ICustomLineHeightData, LineHeightsManager } from './lineHeights.js'; +import { CustomLineHeightData, LineHeightsManager } from './lineHeights.js'; interface IPendingChange { id: string; newAfterLineNumber: number; newHeight: number } interface IPendingRemove { id: string } @@ -95,7 +95,7 @@ export class LinesLayout { private _paddingBottom: number; private _lineHeightsManager: LineHeightsManager; - constructor(lineCount: number, defaultLineHeight: number, paddingTop: number, paddingBottom: number, customLineHeightData: ICustomLineHeightData[]) { + constructor(lineCount: number, defaultLineHeight: number, paddingTop: number, paddingBottom: number, customLineHeightData: CustomLineHeightData[]) { this._instanceId = strings.singleLetterHash(++LinesLayout.INSTANCE_COUNT); this._pendingChanges = new PendingChanges(); this._lastWhitespaceId = 0; @@ -155,7 +155,7 @@ export class LinesLayout { * * @param lineCount New number of lines. */ - public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void { + public onFlushed(lineCount: number, customLineHeightData: CustomLineHeightData[]): void { this._lineCount = lineCount; this._lineHeightsManager = new LineHeightsManager(this._lineHeightsManager.defaultLineHeight, customLineHeightData); } @@ -353,8 +353,9 @@ export class LinesLayout { * * @param fromLineNumber The line number at which the insertion started, inclusive * @param toLineNumber The line number at which the insertion ended, inclusive. + * @param lineHeightsAdded The custom line height data for the inserted lines. */ - public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { + public onLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[]): void { fromLineNumber = fromLineNumber | 0; toLineNumber = toLineNumber | 0; @@ -366,7 +367,7 @@ export class LinesLayout { this._arr[i].afterLineNumber += (toLineNumber - fromLineNumber + 1); } } - this._lineHeightsManager.onLinesInserted(fromLineNumber, toLineNumber); + this._lineHeightsManager.onLinesInserted(fromLineNumber, toLineNumber, lineHeightsAdded); } /** diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 10130ea8dcb..048dc241eae 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -12,7 +12,7 @@ import { IEditorConfiguration } from '../config/editorConfiguration.js'; import { LinesLayout } from './linesLayout.js'; import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewLayout, IViewWhitespaceViewportData, IWhitespaceChangeAccessor, Viewport } from '../viewModel.js'; import { ContentSizeChangedEvent } from '../viewModelEventDispatcher.js'; -import { ICustomLineHeightData } from './lineHeights.js'; +import { CustomLineHeightData } from './lineHeights.js'; const SMOOTH_SCROLLING_TIME = 125; @@ -164,7 +164,7 @@ export class ViewLayout extends Disposable implements IViewLayout { public readonly onDidScroll: Event; public readonly onDidContentSizeChange: Event; - constructor(configuration: IEditorConfiguration, lineCount: number, customLineHeightData: ICustomLineHeightData[], scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { + constructor(configuration: IEditorConfiguration, lineCount: number, customLineHeightData: CustomLineHeightData[], scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); this._configuration = configuration; @@ -237,14 +237,14 @@ export class ViewLayout extends Disposable implements IViewLayout { this._configureSmoothScrollDuration(); } } - public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void { + public onFlushed(lineCount: number, customLineHeightData: CustomLineHeightData[]): void { this._linesLayout.onFlushed(lineCount, customLineHeightData); } public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { this._linesLayout.onLinesDeleted(fromLineNumber, toLineNumber); } - public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { - this._linesLayout.onLinesInserted(fromLineNumber, toLineNumber); + public onLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[]): void { + this._linesLayout.onLinesInserted(fromLineNumber, toLineNumber, lineHeightsAdded); } // ---- end view event handlers diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 954072212bc..f3f33283bfc 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -41,7 +41,7 @@ import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, M import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from './viewModelLines.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { GlyphMarginLanesModel } from './glyphLanesModel.js'; -import { ICustomLineHeightData } from '../viewLayout/lineHeights.js'; +import { CustomLineHeightData } from '../viewLayout/lineHeights.js'; import { TextModelEditSource } from '../textModelEditSource.js'; import { InlineDecoration } from './inlineDecorations.js'; import { ICoordinatesConverter } from '../coordinatesConverter.js'; @@ -196,23 +196,23 @@ export class ViewModel extends Disposable implements IViewModel { this._eventDispatcher.removeViewEventHandler(eventHandler); } - private _getCustomLineHeights(): ICustomLineHeightData[] { + private _getCustomLineHeights(): CustomLineHeightData[] { const allowVariableLineHeights = this._configuration.options.get(EditorOption.allowVariableLineHeights); if (!allowVariableLineHeights) { return []; } - const defaultLineHeight = this._configuration.options.get(EditorOption.lineHeight); const decorations = this.model.getCustomLineHeightsDecorations(this._editorId); - return decorations.map((d) => { - const lineNumber = d.range.startLineNumber; - const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber))); - return { - decorationId: d.id, - startLineNumber: viewRange.startLineNumber, - endLineNumber: viewRange.endLineNumber, - lineHeight: d.options.lineHeight ? d.options.lineHeight * defaultLineHeight : 0 - }; - }); + return CustomLineHeightData.fromDecorations(decorations, this.coordinatesConverter, this._configuration); + } + + private _getCustomLineHeightsForLines(fromLineNumber: number, toLineNumber: number): CustomLineHeightData[] { + const allowVariableLineHeights = this._configuration.options.get(EditorOption.allowVariableLineHeights); + if (!allowVariableLineHeights) { + return []; + } + const modelRange = new Range(fromLineNumber, 1, toLineNumber, this.model.getLineMaxColumn(toLineNumber)); + const decorations = this.model.getCustomLineHeightsDecorationsInRange(modelRange, this._editorId); + return CustomLineHeightData.fromDecorations(decorations, this.coordinatesConverter, this._configuration); } private _updateConfigurationViewLineCountNow(): void { @@ -379,7 +379,7 @@ export class ViewModel extends Disposable implements IViewModel { const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); if (linesInsertedEvent !== null) { eventsCollector.emitViewEvent(linesInsertedEvent); - this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); + this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber, this._getCustomLineHeightsForLines(change.fromLineNumberPostEdit, change.toLineNumberPostEdit)); } hadOtherModelChange = true; break; @@ -394,7 +394,7 @@ export class ViewModel extends Disposable implements IViewModel { } if (linesInsertedEvent) { eventsCollector.emitViewEvent(linesInsertedEvent); - this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); + this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber, this._getCustomLineHeightsForLines(change.lineNumberPostEdit, change.lineNumberPostEdit)); } if (linesDeletedEvent) { eventsCollector.emitViewEvent(linesDeletedEvent); diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 9113fbdff2d..72140c26360 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -130,7 +130,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'foo My First Line', null) + new ModelRawLineChanged(1, 1, 'foo My First Line', null) ], 2, false, @@ -144,8 +144,8 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'My new line', null), - new ModelRawLinesInserted(2, 2, ['No longer First Line'], [null]), + new ModelRawLineChanged(1, 1, 'My new line', null), + new ModelRawLinesInserted(2, 2, 1, ['No longer First Line'], [null]), ], 2, false, @@ -216,7 +216,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'y First Line', null), + new ModelRawLineChanged(1, 1, 'y First Line', null), ], 2, false, @@ -230,7 +230,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, '', null), + new ModelRawLineChanged(1, 1, '', null), ], 2, false, @@ -244,7 +244,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'My Second Line', null), + new ModelRawLineChanged(1, 1, 'My Second Line', null), new ModelRawLinesDeleted(2, 2), ], 2, @@ -259,7 +259,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'My Third Line', null), + new ModelRawLineChanged(1, 1, 'My Third Line', null), new ModelRawLinesDeleted(2, 3), ], 2, diff --git a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts index 19153646275..c94726913ba 100644 --- a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts @@ -195,7 +195,7 @@ suite('Editor ViewLayout - LineHeightsManager', () => { manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); manager.commit(); - manager.onLinesInserted(3, 4); // Insert 2 lines at line 3 + manager.onLinesInserted(3, 4, []); // Insert 2 lines at line 3 assert.strictEqual(manager.heightForLineNumber(5), 10); assert.strictEqual(manager.heightForLineNumber(6), 10); @@ -209,7 +209,7 @@ suite('Editor ViewLayout - LineHeightsManager', () => { manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); manager.commit(); - manager.onLinesInserted(6, 7); // Insert 2 lines at line 6 + manager.onLinesInserted(6, 7, []); // Insert 2 lines at line 6 assert.strictEqual(manager.heightForLineNumber(5), 20); assert.strictEqual(manager.heightForLineNumber(6), 20); diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index 7bf20a78d84..ceb624ac274 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -208,7 +208,7 @@ suite('Editor ViewLayout - LinesLayout', () => { // Insert two lines at the beginning // 10 lines // whitespace: - a(6,10) - linesLayout.onLinesInserted(1, 2); + linesLayout.onLinesInserted(1, 2, []); assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); @@ -909,7 +909,7 @@ suite('Editor ViewLayout - LinesLayout', () => { assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Insert a line before line 1 - linesLayout.onLinesInserted(1, 1); + linesLayout.onLinesInserted(1, 1, []); // whitespaces: d(3, 30), c(4, 20) assert.strictEqual(linesLayout.getWhitespacesCount(), 2); assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index be805ae83df..80497d1a193 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2322,6 +2322,12 @@ declare namespace monaco.editor { * @param ownerId If set, it will ignore decorations belonging to other owners. */ getCustomLineHeightsDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain custom line heights. + * @param range The range to search in + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getCustomLineHeightsDecorationsInRange(range: Range, ownerId?: number): IModelDecoration[]; /** * Normalize a string containing whitespace according to indentation rules (converts to spaces or to tabs). */ diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 1e1a75fa152..f0c6509f8a5 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -50,7 +50,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext throw new Error('Workspace context provider not found'); } const provider = entry.provider as vscode.ChatWorkspaceContextProvider; - const result = (await provider.provideChatContext(token)) ?? []; + const result = (await provider.provideWorkspaceChatContext(token)) ?? (await provider.provideChatContext?.(token)) ?? []; return this._convertItems(handle, result); } @@ -63,7 +63,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext throw new Error('Explicit context provider not found'); } const provider = entry.provider as vscode.ChatExplicitContextProvider; - const result = (await provider.provideChatContext(token)) ?? []; + const result = (await provider.provideExplicitChatContext(token)) ?? (await provider.provideChatContext?.(token)) ?? []; return this._convertItems(handle, result); } @@ -77,7 +77,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!extItem) { throw new Error('Chat context item not found'); } - return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token); + return this._doResolve((provider.resolveExplicitChatContext ?? provider.resolveChatContext).bind(provider), context, extItem, token); } // Resource context provider methods @@ -89,7 +89,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext } const provider = entry.provider as vscode.ChatResourceContextProvider; - const result = await provider.provideChatContext({ resource: URI.revive(options.resource) }, token); + const result = (await provider.provideResourceChatContext({ resource: URI.revive(options.resource) }, token)) ?? (await provider.provideChatContext?.({ resource: URI.revive(options.resource) }, token)); if (!result) { return undefined; } @@ -109,7 +109,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext command: result.command ? { id: result.command.command } : undefined }; if (options.withValue && !item.value) { - const resolved = await provider.resolveChatContext(result, token); + const resolved = await (provider.resolveResourceChatContext ?? provider.resolveChatContext).bind(provider)(result, token); item.value = resolved?.value; item.tooltip = resolved?.tooltip ? MarkdownString.from(resolved.tooltip) : item.tooltip; } @@ -127,7 +127,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!extItem) { throw new Error('Chat context item not found'); } - return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token); + return this._doResolve((provider.resolveResourceChatContext ?? provider.resolveChatContext).bind(provider), context, extItem, token); } // Command execution @@ -209,7 +209,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (provider.provideWorkspaceChatContext) { const workspaceProvider: vscode.ChatWorkspaceContextProvider = { onDidChangeWorkspaceChatContext: provider.onDidChangeWorkspaceChatContext, - provideChatContext: (token) => provider.provideWorkspaceChatContext!(token) + provideWorkspaceChatContext: (token) => provider.provideWorkspaceChatContext!(token) }; disposables.push(this.registerChatWorkspaceContextProvider(id, workspaceProvider)); } @@ -217,8 +217,8 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext // Register explicit context provider if the provider supports it if (provider.provideChatContextExplicit) { const explicitProvider: vscode.ChatExplicitContextProvider = { - provideChatContext: (token) => provider.provideChatContextExplicit!(token), - resolveChatContext: provider.resolveChatContext + provideExplicitChatContext: (token) => provider.provideChatContextExplicit!(token), + resolveExplicitChatContext: provider.resolveChatContext ? (context, token) => provider.resolveChatContext!(context, token) : (context) => context }; @@ -228,8 +228,8 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext // Register resource context provider if the provider supports it and has a selector if (provider.provideChatContextForResource && selector) { const resourceProvider: vscode.ChatResourceContextProvider = { - provideChatContext: (options, token) => provider.provideChatContextForResource!(options, token), - resolveChatContext: provider.resolveChatContext + provideResourceChatContext: (options, token) => provider.provideChatContextForResource!(options, token), + resolveResourceChatContext: provider.resolveChatContext ? (context, token) => provider.resolveChatContext!(context, token) : (context) => context }; @@ -315,7 +315,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext return; } const provideWorkspaceContext = async () => { - const workspaceContexts = await provider.provideChatContext(CancellationToken.None); + const workspaceContexts = await provider.provideWorkspaceChatContext(CancellationToken.None); const resolvedContexts = this._convertItems(handle, workspaceContexts ?? []); return this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); }; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 6978ffacbf0..60ffabb3a63 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -77,7 +77,7 @@ export class ActivitybarPart extends Part { viewContainersWorkspaceStateKey: ActivitybarPart.viewContainersWorkspaceStateKey, orientation: ActionsOrientation.VERTICAL, icon: true, - iconSize: 24, + iconSize: 16, activityHoverOptions: { position: () => this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT, }, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 4a6166dceea..3c23b729b2a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -88,6 +88,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'age content.push(localize('chat.focusMostRecentTerminal', 'To focus the last chat terminal that ran a tool, invoke the Focus Most Recent Chat Terminal command{0}.', ``)); content.push(localize('chat.focusMostRecentTerminalOutput', 'To focus the output from the last chat terminal tool, invoke the Focus Most Recent Chat Terminal Output command{0}.', ``)); content.push(localize('chat.focusQuestionCarousel', 'When a chat question appears, toggle focus between the question and the chat input{0}.', '')); + content.push(localize('chat.focusTip', 'When a tip appears, toggle focus between the tip and the chat input{0}.', '')); } if (type === 'editsView' || type === 'agentView') { if (type === 'agentView') { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 980ba420f12..dc57a988509 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -809,6 +809,37 @@ export function registerChatActions() { } }); + registerAction2(class FocusTipAction extends Action2 { + static readonly ID = 'workbench.action.chat.focusTip'; + + constructor() { + super({ + id: FocusTipAction.ID, + title: localize2('interactiveSession.focusTip.label', "Chat: Toggle Focus Between Tip and Input"), + category: CHAT_CATEGORY, + f1: true, + precondition: ChatContextKeys.inChatSession, + keybinding: [{ + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Slash, + when: ContextKeyExpr.or( + ChatContextKeys.inChatSession, + ChatContextKeys.inChatTip + ), + }] + }); + } + + run(accessor: ServicesAccessor): void { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + + if (!widget || !widget.toggleTipFocus()) { + alert(localize('chat.tip.focusUnavailable', "No chat tip.")); + } + } + }); + registerAction2(class ShowContextUsageAction extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts index e4eb46e3aab..bb72913ae98 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts @@ -325,8 +325,13 @@ async function collectHooksStatus( const discoveryInfo = await promptsService.getPromptDiscoveryInfo(type, token); const files = discoveryInfo.files.map(convertDiscoveryResultToFileStatus); + // Collect URIs of files skipped due to disableAllHooks so we can show their hidden hooks + const disabledFileUris = discoveryInfo.files + .filter(f => f.status === 'skipped' && f.skipReason === 'all-hooks-disabled') + .map(f => f.uri); + // Parse hook files to extract individual hooks grouped by lifecycle - const parsedHooks = await parseHookFiles(promptsService, fileService, labelService, pathService, workspaceContextService, remoteAgentService, token); + const parsedHooks = await parseHookFiles(promptsService, fileService, labelService, pathService, workspaceContextService, remoteAgentService, token, disabledFileUris); return { type, paths, files, enabled, parsedHooks }; } @@ -341,7 +346,8 @@ async function parseHookFiles( pathService: IPathService, workspaceContextService: IWorkspaceContextService, remoteAgentService: IRemoteAgentService, - token: CancellationToken + token: CancellationToken, + additionalDisabledFileUris?: URI[] ): Promise { // Get workspace root and user home for path resolution const workspaceFolder = workspaceContextService.getWorkspace().folders[0]; @@ -354,7 +360,7 @@ async function parseHookFiles( const targetOS = remoteEnv?.os ?? OS; // Use the shared helper - return parseAllHookFiles(promptsService, fileService, labelService, workspaceRootUri, userHome, targetOS, token); + return parseAllHookFiles(promptsService, fileService, labelService, workspaceRootUri, userHome, targetOS, token, { additionalDisabledFileUris }); } /** @@ -442,6 +448,8 @@ function getSkipReasonMessage(skipReason: PromptFileSkipReason | undefined, erro return errorMessage ?? nls.localize('status.parseError', 'Parse error'); case 'disabled': return nls.localize('status.typeDisabled', 'Disabled'); + case 'all-hooks-disabled': + return nls.localize('status.allHooksDisabled', 'All hooks disabled via disableAllHooks'); default: return errorMessage ?? nls.localize('status.unknownError', 'Unknown error'); } @@ -735,16 +743,22 @@ export function formatStatusOutput( const fileHooks = hooksByFile.get(fileKey)!; const firstHook = fileHooks[0]; const filePath = getRelativePath(firstHook.fileUri, workspaceFolders); + const fileDisabled = fileHooks[0].disabled; - // File as clickable link - lines.push(`[${firstHook.filePath}](${filePath})
`); + // File as clickable link, with note if hooks are disabled via flag + if (fileDisabled) { + lines.push(`[${firstHook.filePath}](${filePath}) - *${nls.localize('status.allHooksDisabledLabel', 'all hooks disabled via disableAllHooks')}*
`); + } else { + lines.push(`[${firstHook.filePath}](${filePath})
`); + } // Flatten hooks with their lifecycle label for (let i = 0; i < fileHooks.length; i++) { const hook = fileHooks[i]; const isLast = i === fileHooks.length - 1; const prefix = isLast ? TREE_END : TREE_BRANCH; - lines.push(`${prefix} ${hook.hookTypeLabel}: \`${hook.commandLabel}\`
`); + const disabledPrefix = hook.disabled ? `${ICON_ERROR} ` : ''; + lines.push(`${prefix} ${disabledPrefix}${hook.hookTypeLabel}: \`${hook.commandLabel}\`
`); } } hasContent = true; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index db362272166..f51de3f44f1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -356,7 +356,7 @@ configurationRegistry.registerConfiguration({ '**/*.lock': false, // yarn.lock, bun.lock, etc. '**/*-lock.{yaml,json}': false, // pnpm-lock.yaml, package-lock.json }, - markdownDescription: nls.localize('chat.tools.autoApprove.edits', "Controls whether edits made by chat are automatically approved. The default is to approve all edits except those made to certain files which have the potential to cause immediate unintended side-effects, such as `**/.vscode/*.json`.\n\nSet to `true` to automatically approve edits to matching files, `false` to always require explicit approval. The last pattern matching a given file will determine whether the edit is automatically approved."), + markdownDescription: nls.localize('chat.tools.autoApprove.edits', "Controls whether edits made by the agent are automatically approved. The default is to approve all edits except those made to certain files which have the potential to cause immediate unintended side-effects, such as `**/.vscode/*.json`.\n\nSet to `true` to automatically approve edits to matching files, `false` to always require explicit approval. The last pattern matching a given file will determine whether the edit is automatically approved."), type: 'object', additionalProperties: { type: 'boolean', @@ -1408,6 +1408,16 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { }, async () => { await instantiationService.invokeFunction(showConfigureHooksQuickPick); })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'debug', + detail: nls.localize('debug', "Show Chat Debug View"), + sortText: 'z3_debug', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async () => { + await commandService.executeCommand('github.copilot.debug.showChatLogView'); + })); this._store.add(slashCommandService.registerSlashCommand({ command: 'agents', detail: nls.localize('agents', "Configure custom agents"), diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 2d2b307f23e..fc4423e96d1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -386,6 +386,12 @@ export interface IChatWidget { * @returns Whether the operation succeeded (i.e., the focus was toggled). */ toggleQuestionCarouselFocus(): boolean; + /** + * Toggles focus between the tip widget and the chat input. + * Returns false if no tip is visible. + * @returns Whether the operation succeeded (i.e., the focus was toggled). + */ + toggleTipFocus(): boolean; hasInputFocus(): boolean; getModeRequestOptions(): Partial; getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts index 87acdace945..e6dd6668f35 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { findNodeAtLocation, Node, parseTree } from '../../../../../base/common/json.js'; +import { findNodeAtLocation, Node, parse as parseJSONC, parseTree } from '../../../../../base/common/json.js'; import { ITextEditorSelection } from '../../../../../platform/editor/common/editor.js'; import { URI } from '../../../../../base/common/uri.js'; import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; @@ -11,7 +11,7 @@ import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { formatHookCommandLabel, HOOK_TYPES, HookType, IHookCommand } from '../../common/promptSyntax/hookSchema.js'; -import { parseHooksFromFile } from '../../common/promptSyntax/hookCompatibility.js'; +import { parseHooksFromFile, parseHooksIgnoringDisableAll } from '../../common/promptSyntax/hookCompatibility.js'; import * as nls from '../../../../../nls.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { OperatingSystem } from '../../../../../base/common/platform.js'; @@ -126,6 +126,13 @@ export interface IParsedHook { index: number; /** The original hook type ID as it appears in the JSON file */ originalHookTypeId: string; + /** If true, this hook is disabled via `disableAllHooks: true` in its file */ + disabled?: boolean; +} + +export interface IParseAllHookFilesOptions { + /** Additional file URIs to parse (e.g., files skipped due to disableAllHooks) */ + additionalDisabledFileUris?: readonly URI[]; } /** @@ -139,7 +146,8 @@ export async function parseAllHookFiles( workspaceRootUri: URI | undefined, userHome: string, os: OperatingSystem, - token: CancellationToken + token: CancellationToken, + options?: IParseAllHookFilesOptions ): Promise { const hookFiles = await promptsService.listPromptFiles(PromptsType.hook, token); const parsedHooks: IParsedHook[] = []; @@ -147,7 +155,7 @@ export async function parseAllHookFiles( for (const hookFile of hookFiles) { try { const content = await fileService.readFile(hookFile.uri); - const json = JSON.parse(content.value.toString()); + const json = parseJSONC(content.value.toString()); // Use format-aware parsing const { hooks } = parseHooksFromFile(hookFile.uri, json, workspaceRootUri, userHome); @@ -179,5 +187,44 @@ export async function parseAllHookFiles( } } + // Parse additional disabled files (e.g., files with disableAllHooks: true) + // These are parsed ignoring the disableAllHooks flag so we can show their hooks as disabled + if (options?.additionalDisabledFileUris) { + for (const uri of options.additionalDisabledFileUris) { + try { + const content = await fileService.readFile(uri); + const json = parseJSONC(content.value.toString()); + + // Parse hooks ignoring disableAllHooks - use the underlying format parsers directly + const { hooks } = parseHooksIgnoringDisableAll(uri, json, workspaceRootUri, userHome); + + for (const [hookType, { hooks: commands, originalId }] of hooks) { + const hookTypeMeta = HOOK_TYPES.find(h => h.id === hookType); + if (!hookTypeMeta) { + continue; + } + + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + const commandLabel = formatHookCommandLabel(command, os) || nls.localize('commands.hook.emptyCommand', '(empty command)'); + parsedHooks.push({ + hookType, + hookTypeLabel: hookTypeMeta.label, + command, + commandLabel, + fileUri: uri, + filePath: labelService.getUriLabel(uri, { relative: true }), + index: i, + originalHookTypeId: originalId, + disabled: true + }); + } + } + } catch (error) { + console.error('Failed to read or parse disabled hook file', uri.toString(), error); + } + } + } + return parsedHooks; } diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 4bc871cae89..80612e6daf3 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -47,7 +47,7 @@ import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToo import { chatSessionResourceToId } from '../../common/model/chatUri.js'; import { HookType } from '../../common/promptSyntax/hookSchema.js'; import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; -import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, IExternalPreToolUseHookResult, ILanguageModelToolsService, IPreparedToolInvocation, isToolSet, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolInvokedEvent, IToolResult, IToolResultInputOutputDetails, IToolSet, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, toolMatchesModel, ToolSet, ToolSetForModel, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js'; +import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, IExternalPreToolUseHookResult, ILanguageModelToolsService, IPreparedToolInvocation, isToolSet, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolInvokedEvent, IToolResult, IToolResultInputOutputDetails, IToolSet, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolInvocationPresentation, toolMatchesModel, ToolSet, ToolSetForModel, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js'; import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js'; const jsonSchemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -209,6 +209,13 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (agentModeEnabled !== false) { return true; } + + // Internal tools that explicitly cannot be referenced in prompts are always permitted + // since they are infrastructure tools (e.g. inline_chat_exit), not user-facing agent tools + if (!isToolSet(toolOrToolSet) && toolOrToolSet.canBeReferencedInPrompt === false && toolOrToolSet.source.type === 'internal') { + return true; + } + const permittedInternalToolSetIds = [SpecedToolAliases.read, SpecedToolAliases.search, SpecedToolAliases.web]; if (isToolSet(toolOrToolSet)) { const permitted = toolOrToolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolOrToolSet.referenceName); @@ -377,6 +384,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (toolData) { if (pendingInvocation) { + pendingInvocation.presentation = ToolInvocationPresentation.Hidden; pendingInvocation.cancelFromStreaming(ToolConfirmKind.Denied, reason); } else if (request) { const cancelledInvocation = ChatToolInvocation.createCancelled( @@ -385,6 +393,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo ToolConfirmKind.Denied, reason ); + cancelledInvocation.presentation = ToolInvocationPresentation.Hidden; this._chatService.appendProgress(request, cancelledInvocation); } } @@ -726,17 +735,49 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } const fullReferenceName = getToolFullReferenceName(tool.data); const hookReason = hookResult.permissionDecisionReason; - const baseMessage = localize('hookRequiresConfirmation.message', "{0} hook confirmation required", HookType.PreToolUse); + const hookNote = hookReason + ? localize('hookRequiresConfirmation.messageWithReason', "{0} hook required confirmation: {1}", HookType.PreToolUse, hookReason) + : localize('hookRequiresConfirmation.message', "{0} hook required confirmation", HookType.PreToolUse); preparedInvocation.confirmationMessages = { ...preparedInvocation.confirmationMessages, title: localize('hookRequiresConfirmation.title', "Use the '{0}' tool?", fullReferenceName), - message: new MarkdownString(hookReason ? `${baseMessage}\n\n${hookReason}` : baseMessage), + message: new MarkdownString(`_${hookNote}_`), allowAutoConfirm: false, }; preparedInvocation.toolSpecificData = { kind: 'input', rawInput: dto.parameters, }; + } else { + // Tool already has its own confirmation - prepend hook note + const hookReason = hookResult.permissionDecisionReason; + const hookNote = hookReason + ? localize('hookRequiresConfirmation.note', "{0} hook required confirmation: {1}", HookType.PreToolUse, hookReason) + : localize('hookRequiresConfirmation.noteNoReason', "{0} hook required confirmation", HookType.PreToolUse); + + const existing = preparedInvocation.confirmationMessages!; + if (preparedInvocation.toolSpecificData?.kind === 'terminal') { + // Terminal tools render message as hover only; use disclaimer for visible text + const existingDisclaimerText = existing.disclaimer + ? (typeof existing.disclaimer === 'string' ? existing.disclaimer : existing.disclaimer.value) + : undefined; + const combinedDisclaimer = existingDisclaimerText + ? `${hookNote}\n\n${existingDisclaimerText}` + : hookNote; + preparedInvocation.confirmationMessages = { + ...existing, + disclaimer: combinedDisclaimer, + allowAutoConfirm: false, + }; + } else { + // Edit/other tools: prepend hook note to the message body + const msgText = typeof existing.message === 'string' ? existing.message : existing.message?.value ?? ''; + preparedInvocation.confirmationMessages = { + ...existing, + message: new MarkdownString(`_${hookNote}_\n\n${msgText}`), + allowAutoConfirm: false, + }; + } } return { autoConfirmed: undefined, preparedInvocation }; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatHookContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatHookContentPart.ts index 464f0791383..a9f8451881a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatHookContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatHookContentPart.ts @@ -9,7 +9,7 @@ import { localize } from '../../../../../../nls.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IChatHookPart } from '../../../common/chatService/chatService.js'; import { IChatRendererContent } from '../../../common/model/chatViewModel.js'; -import { HOOK_TYPES, HookTypeValue } from '../../../common/promptSyntax/hookSchema.js'; +import { HookType, HOOK_TYPES, HookTypeValue } from '../../../common/promptSyntax/hookSchema.js'; import { ChatTreeItem } from '../../chat.js'; import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; @@ -29,16 +29,23 @@ export class ChatHookContentPart extends ChatCollapsibleContentPart implements I const hookTypeLabel = getHookTypeLabel(hookPart.hookType); const isStopped = !!hookPart.stopReason; const isWarning = !!hookPart.systemMessage; + const toolName = hookPart.toolDisplayName; const title = isStopped - ? localize('hook.title.stopped', "Blocked by {0} hook", hookTypeLabel) - : localize('hook.title.warning', "Warning from {0} hook", hookTypeLabel); + ? (toolName + ? localize('hook.title.stoppedWithTool', "Blocked {0} - {1} hook", toolName, hookTypeLabel) + : localize('hook.title.stopped', "Blocked by {0} hook", hookTypeLabel)) + : (toolName + ? localize('hook.title.warningWithTool', "Warning for {0} - {1} hook", toolName, hookTypeLabel) + : localize('hook.title.warning', "Warning from {0} hook", hookTypeLabel)); super(title, context, undefined, hoverService); - this.icon = isStopped ? Codicon.circleSlash : isWarning ? Codicon.warning : Codicon.check; + this.icon = isStopped ? Codicon.error : isWarning ? Codicon.warning : Codicon.check; if (isStopped) { this.domNode.classList.add('chat-hook-outcome-blocked'); + } else if (isWarning) { + this.domNode.classList.add('chat-hook-outcome-warning'); } this.setExpanded(false); @@ -50,7 +57,10 @@ export class ChatHookContentPart extends ChatCollapsibleContentPart implements I if (this.hookPart.stopReason) { const reasonElement = $('.chat-hook-reason', undefined, this.hookPart.stopReason); content.appendChild(reasonElement); - } else if (this.hookPart.systemMessage) { + } + + const isToolHook = this.hookPart.hookType === HookType.PreToolUse || this.hookPart.hookType === HookType.PostToolUse; + if (this.hookPart.systemMessage && (isToolHook || !this.hookPart.stopReason)) { const messageElement = $('.chat-hook-message', undefined, this.hookPart.systemMessage); content.appendChild(messageElement); } @@ -64,6 +74,7 @@ export class ChatHookContentPart extends ChatCollapsibleContentPart implements I } return other.hookType === this.hookPart.hookType && other.stopReason === this.hookPart.stopReason && - other.systemMessage === this.hookPart.systemMessage; + other.systemMessage === this.hookPart.systemMessage && + other.toolDisplayName === this.hookPart.toolDisplayName; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index 86a60cfacfd..5e03ac8c247 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -16,7 +16,7 @@ import { localize } from '../../../../../../nls.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; -import { IChatMarkdownContent, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; +import { IChatHookPart, IChatMarkdownContent, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; import { IChatRendererContent } from '../../../common/model/chatViewModel.js'; import { IRunSubagentToolInputParams } from '../../../common/tools/builtinTools/runSubagentTool.js'; import { CodeBlockModelCollection } from '../../../common/widget/codeBlockModelCollection.js'; @@ -51,7 +51,16 @@ interface ILazyMarkdownItem { lazy: Lazy<{ domNode: HTMLElement; disposable?: IDisposable }>; } -type ILazyItem = ILazyToolItem | ILazyMarkdownItem; +/** + * Represents a lazy hook item (blocked/warning) that will be rendered when expanded. + */ +interface ILazyHookItem { + kind: 'hook'; + lazy: Lazy<{ domNode: HTMLElement; disposable?: IDisposable }>; + hookPart: IChatHookPart; +} + +type ILazyItem = ILazyToolItem | ILazyMarkdownItem | ILazyHookItem; /** * This is generally copied from ChatThinkingContentPart. We are still experimenting with both UIs so I'm not @@ -587,6 +596,58 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } } + /** + * Appends a hook item (blocked/warning) to the subagent content part. + */ + public appendHookItem( + factory: () => { domNode: HTMLElement; disposable?: IDisposable }, + hookPart: IChatHookPart + ): void { + if (this.isExpanded() || this.hasExpandedOnce) { + const result = factory(); + this.appendHookItemToDOM(result.domNode, hookPart); + if (result.disposable) { + this._register(result.disposable); + } + } else { + const item: ILazyHookItem = { + kind: 'hook', + lazy: new Lazy(factory), + hookPart, + }; + this.lazyItems.push(item); + } + } + + /** + * Appends a hook item's DOM node to the wrapper. + */ + private appendHookItemToDOM(domNode: HTMLElement, hookPart: IChatHookPart): void { + const itemWrapper = $('.chat-thinking-tool-wrapper'); + const icon = hookPart.stopReason ? Codicon.error : Codicon.warning; + const iconElement = createThinkingIcon(icon); + itemWrapper.appendChild(iconElement); + itemWrapper.appendChild(domNode); + + // Treat hook items as tool items for visibility purposes + if (!this.hasToolItems) { + this.hasToolItems = true; + if (this.wrapper) { + this.wrapper.style.display = ''; + } + } + + if (this.wrapper) { + if (this.resultContainer) { + this.wrapper.insertBefore(itemWrapper, this.resultContainer); + } else { + this.wrapper.appendChild(itemWrapper); + } + } + this.lastItemWrapper = itemWrapper; + this.layoutScheduler.schedule(); + } + /** * Appends a markdown item's DOM node to the wrapper. */ @@ -705,6 +766,12 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen if (result.disposable) { this._register(result.disposable); } + } else if (item.kind === 'hook') { + const result = item.lazy.value; + this.appendHookItemToDOM(result.domNode, item.hookPart); + if (result.disposable) { + this._register(result.disposable); + } } } @@ -759,6 +826,11 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen return true; } + // Match hook parts with the same subAgentInvocationId to keep them grouped in the subagent dropdown + if (other.kind === 'hook' && other.subAgentInvocationId) { + return this.subAgentInvocationId === other.subAgentInvocationId; + } + // Match subagent tool invocations with the same subAgentInvocationId to keep them grouped if ((other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') && (other.subAgentInvocationId || ChatSubagentContentPart.isParentSubagentTool(other))) { // For parent subagent tool, use toolCallId as the effective ID diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index ce9c70c41d7..4f926a701d4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -765,6 +765,12 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen - For reasoning/thinking: "Considered", "Planned", "Analyzed", "Reviewed", "Evaluated" - Choose the synonym that best fits the context + PRIORITY RULE - BLOCKED/DENIED CONTENT: + - If any item mentions being "blocked" (e.g. "Tried to use X, but was blocked"), it MUST be reflected in the title + - Blocked content takes priority over all other tool calls + - Use natural phrasing like "Tried to , but was blocked" or "Attempted but was denied" + - If there are both blocked items AND normal tool calls, mention both: e.g. "Tried to run terminal but was blocked, edited file.ts" + RULES FOR TOOL CALLS: 1. If the SAME file was both edited AND read: Use a combined phrase like "Reviewed and updated " 2. If exactly ONE file was edited: Start with an edit synonym + "" (include actual filename) @@ -804,6 +810,12 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen - "Edited Button.tsx, Edited Button.css, Edited index.ts" → "Modified 3 files" - "Searched codebase for error handling" → "Looked up error handling" + EXAMPLES WITH BLOCKED CONTENT: + - "Tried to use Run in Terminal, but was blocked" → "Tried to run command, but was blocked" + - "Tried to use Run in Terminal, but was blocked, Edited config.ts" → "Tried to run command but was blocked, edited config.ts" + - "Tried to use Edit File, but was blocked, Tried to use Run in Terminal, but was blocked" → "Tried to use 2 tools, but was blocked" + - "Used Read File, but received a warning, Edited utils.ts" → "Read file with a warning, edited utils.ts" + EXAMPLES WITH REASONING HEADERS (no tools): - "Analyzing component architecture" → "Considered component architecture" - "Planning refactor strategy" → "Planned refactor strategy" @@ -1160,7 +1172,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen toolCallLabel = localize('chat.thinking.editingFile', 'Edited file'); } } else { - toolCallLabel = `Invoked \`${toolInvocationId}\``; + toolCallLabel = toolInvocationId; } // Add tool call to extracted titles for LLM title generation @@ -1207,6 +1219,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen const terminalData = (toolInvocationOrMarkdown as IChatToolInvocation | IChatToolInvocationSerialized).toolSpecificData as { kind: 'terminal'; terminalCommandState?: { exitCode?: number } }; const exitCode = terminalData?.terminalCommandState?.exitCode; icon = exitCode !== undefined && exitCode !== 0 ? Codicon.error : Codicon.terminal; + } else if (content.classList.contains('chat-hook-outcome-blocked')) { + icon = Codicon.error; + } else if (content.classList.contains('chat-hook-outcome-warning')) { + icon = Codicon.warning; } else { icon = toolInvocationId ? getToolInvocationIcon(toolInvocationId) : Codicon.tools; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts index df83eb5d194..7a5ba17dcaa 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts @@ -6,17 +6,19 @@ import './media/chatTipContent.css'; import * as dom from '../../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; +import { status } from '../../../../../../base/browser/ui/aria/aria.js'; import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; -import { localize2 } from '../../../../../../nls.js'; +import { localize, localize2 } from '../../../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import { IChatTip, IChatTipService } from '../../chatTipService.js'; const $ = dom.$; @@ -29,6 +31,8 @@ export class ChatTipContentPart extends Disposable { private readonly _renderedContent = this._register(new MutableDisposable()); + private readonly _inChatTipContextKey: IContextKey; + constructor( tip: IChatTip, private readonly _renderer: IMarkdownRenderer, @@ -41,6 +45,16 @@ export class ChatTipContentPart extends Disposable { super(); this.domNode = $('.chat-tip-widget'); + this.domNode.tabIndex = 0; + this.domNode.setAttribute('role', 'region'); + this.domNode.setAttribute('aria-roledescription', localize('chatTipRoleDescription', "tip")); + + this._inChatTipContextKey = ChatContextKeys.inChatTip.bindTo(this._contextKeyService); + const focusTracker = this._register(dom.trackFocus(this.domNode)); + this._register(focusTracker.onDidFocus(() => this._inChatTipContextKey.set(true))); + this._register(focusTracker.onDidBlur(() => this._inChatTipContextKey.set(false))); + this._register({ dispose: () => this._inChatTipContextKey.reset() }); + this._renderTip(tip); this._register(this._chatTipService.onDidDismissTip(() => { @@ -69,12 +83,27 @@ export class ChatTipContentPart extends Disposable { })); } + hasFocus(): boolean { + return dom.isAncestorOfActiveElement(this.domNode); + } + + focus(): void { + this.domNode.focus(); + } + private _renderTip(tip: IChatTip): void { dom.clearNode(this.domNode); this.domNode.appendChild(renderIcon(Codicon.lightbulb)); const markdownContent = this._renderer.render(tip.content); this._renderedContent.value = markdownContent; this.domNode.appendChild(markdownContent.element); + const textContent = markdownContent.element.textContent ?? localize('chatTip', "Chat tip"); + const hasLink = /\[.*?\]\(.*?\)/.test(tip.content.value); + const ariaLabel = hasLink + ? localize('chatTipWithAction', "{0} Tab to the action.", textContent) + : textContent; + this.domNode.setAttribute('aria-label', ariaLabel); + status(ariaLabel); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css index c06e49192d5..3a30dc1e68a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css @@ -7,6 +7,17 @@ color: var(--vscode-notificationsWarningIcon-foreground) !important; } +.chat-thinking-box .chat-used-context.chat-hook-outcome-blocked, +.chat-thinking-box .chat-used-context.chat-hook-outcome-warning { + padding: 4px 12px 4px 22px; + margin-bottom: 0; +} + +.chat-thinking-box .chat-used-context.chat-hook-outcome-blocked > .chat-used-context-label .codicon, +.chat-thinking-box .chat-used-context.chat-hook-outcome-warning > .chat-used-context-label .codicon { + display: none; +} + .chat-hook-details { display: flex; flex-direction: column; @@ -14,8 +25,20 @@ padding: 8px 12px; } -.chat-hook-message, .chat-hook-reason { +.chat-hook-reason { + font-size: var(--vscode-chat-font-size-body-s); + padding: 4px 10px; +} + +.chat-hook-message { font-size: var(--vscode-chat-font-size-body-s); padding: 4px 10px; color: var(--vscode-descriptionForeground); } + +/* When both reason and message are shown, add a subtle separator */ +.chat-hook-reason + .chat-hook-message { + border-top: 1px solid var(--vscode-chat-requestBorder, var(--vscode-editorWidget-border)); + margin-top: 2px; + padding-top: 6px; +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 38d1242b302..c533f527c54 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -108,6 +108,7 @@ import { IChatTipService } from '../chatTipService.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { ChatHookContentPart } from './chatContentParts/chatHookContentPart.js'; import { ChatPendingDragController } from './chatPendingDragAndDrop.js'; +import { HookType } from '../../common/promptSyntax/hookSchema.js'; const $ = dom.$; @@ -183,6 +184,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer>(); + private _activeTipPart: ChatTipContentPart | undefined; + private readonly _notifiedQuestionCarousels = new WeakSet(); private readonly _questionCarouselToast = this._register(new DisposableStore()); @@ -300,6 +303,18 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.chatTipService.getNextTip(element.id, element.timestamp, this.contextKeyService), ); templateData.value.appendChild(tipPart.domNode); + this._activeTipPart = tipPart; templateData.elementDisposables.add(tipPart); templateData.elementDisposables.add(tipPart.onDidHide(() => { tipPart.domNode.remove(); + if (this._activeTipPart === tipPart) { + this._activeTipPart = undefined; + } })); + templateData.elementDisposables.add({ + dispose: () => { + if (this._activeTipPart === tipPart) { + this._activeTipPart = undefined; + } + } + }); } let inlineSlashCommandRendered = false; @@ -1226,7 +1252,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.agent.thinking.collapsedTools'); @@ -1428,6 +1455,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer other.kind === 'hook' && other.hookType === hookPart.hookType); } - return this.renderNoContent(other => other.kind === 'hook' && other.hookType === hookPart.hookType); + + if (hookPart.subAgentInvocationId) { + const subagentPart = this.getSubagentPart(templateData.renderedParts, hookPart.subAgentInvocationId); + if (subagentPart) { + subagentPart.appendHookItem(() => { + const part = this.instantiationService.createInstance(ChatHookContentPart, hookPart, context); + return { domNode: part.domNode, disposable: part }; + }, hookPart); + return this.renderNoContent(other => other.kind === 'hook' && other.hookType === hookPart.hookType && other.subAgentInvocationId === hookPart.subAgentInvocationId); + } + } + + // Only pin preTool/postTool hooks into the thinking part + const shouldPinToThinking = hookPart.hookType === HookType.PreToolUse || hookPart.hookType === HookType.PostToolUse; + if (shouldPinToThinking) { + const hookTitle = hookPart.stopReason + ? (hookPart.toolDisplayName + ? localize('hook.thinking.blocked', "Blocked {0}", hookPart.toolDisplayName) + : localize('hook.thinking.blockedGeneric', "Blocked by hook")) + : (hookPart.toolDisplayName + ? localize('hook.thinking.warning', "Used {0}, but received a warning", hookPart.toolDisplayName) + : localize('hook.thinking.warningGeneric', "Tool call received a warning")); + + let thinkingPart = this.getLastThinkingPart(templateData.renderedParts); + if (!thinkingPart) { + // Create a thinking part if one doesn't exist yet (e.g. hook arrives before/with its tool in the same turn) + const newThinking = this.renderThinkingPart({ kind: 'thinking' }, context, templateData); + if (newThinking instanceof ChatThinkingContentPart) { + thinkingPart = newThinking; + } + } + + if (thinkingPart) { + thinkingPart.appendItem(() => { + const part = this.instantiationService.createInstance(ChatHookContentPart, hookPart, context); + return { domNode: part.domNode, disposable: part }; + }, hookTitle, undefined, templateData.value); + return thinkingPart; + } + } + + const part = this.instantiationService.createInstance(ChatHookContentPart, hookPart, context); + return part; } private renderPullRequestContent(pullRequestContent: IChatPullRequestContent, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): IChatContentPart | undefined { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index 4e33ef0c176..50b40d59419 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -789,6 +789,21 @@ export class ChatListWidget extends Disposable { return this._renderer.editorsInUse(); } + /** + * Whether the active tip currently has focus. + */ + hasTipFocus(): boolean { + return this._renderer.hasTipFocus(); + } + + /** + * Focus the active tip, if any. + * @returns Whether a tip was focused. + */ + focusTip(): boolean { + return this._renderer.focusTip(); + } + /** * Get template data for a request ID. */ diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 8fa0fbb402f..13c0fb84ae1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -742,6 +742,15 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.input.focusQuestionCarousel(); } + toggleTipFocus(): boolean { + if (this.listWidget.hasTipFocus()) { + this.focusInput(); + return true; + } + + return this.listWidget.focusTip(); + } + hasInputFocus(): boolean { return this.input.hasFocus(); } diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index cb507daf273..94bb20e75af 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -41,6 +41,7 @@ export namespace ChatContextKeys { 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 inChatTodoList = new RawContextKey('inChatTodoList', false, { type: 'boolean', description: localize('inChatTodoList', "True when focus is in the chat todo list.") }); + export const inChatTip = new RawContextKey('inChatTip', false, { type: 'boolean', description: localize('inChatTip', "True when focus is in a chat tip.") }); 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).") }); diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 657c6d6ac1e..47c0fc70084 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -434,7 +434,11 @@ export interface IChatHookPart { stopReason?: string; /** Warning/system message from the hook, shown to the user */ systemMessage?: string; + /** Display name of the tool that was affected by the hook */ + toolDisplayName?: string; metadata?: { readonly [key: string]: unknown }; + /** If set, this hook was executed within a subagent invocation and should be grouped with it. */ + subAgentInvocationId?: string; } export interface IChatTerminalToolInvocationData { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts index 5f2079ea401..c159acfa4c3 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts @@ -53,6 +53,20 @@ export function getClaudeHookTypeName(hookType: HookType): string | undefined { return getHookTypeToClaudeNameMap().get(hookType); } +/** + * Result of parsing Claude hooks file. + */ +export interface IParseClaudeHooksResult { + /** + * The parsed hooks by type. + */ + readonly hooks: Map; + /** + * Whether all hooks from this file were disabled via `disableAllHooks: true`. + */ + readonly disabledAllHooks: boolean; +} + /** * Parses hooks from a Claude settings.json file. * Claude format: @@ -70,23 +84,31 @@ export function getClaudeHookTypeName(hookType: HookType): string | undefined { * "PreToolUse": [{ "type": "command", "command": "..." }] * } * } + * + * If the file has `disableAllHooks: true` at the top level, all hooks are filtered out. */ export function parseClaudeHooks( json: unknown, workspaceRootUri: URI | undefined, userHome: string -): Map { +): IParseClaudeHooksResult { const result = new Map(); if (!json || typeof json !== 'object') { - return result; + return { hooks: result, disabledAllHooks: false }; } const root = json as Record; + + // Check for disableAllHooks property at the top level + if (root.disableAllHooks === true) { + return { hooks: result, disabledAllHooks: true }; + } + const hooks = root.hooks; if (!hooks || typeof hooks !== 'object') { - return result; + return { hooks: result, disabledAllHooks: false }; } const hooksObj = hooks as Record; @@ -140,7 +162,7 @@ export function parseClaudeHooks( } } - return result; + return { hooks: result, disabledAllHooks: false }; } /** @@ -158,7 +180,5 @@ function resolveClaudeCommand( return undefined; } - // Add type if missing for resolveHookCommand - const normalized = { ...raw, type: 'command' }; - return resolveHookCommand(normalized, workspaceRootUri, userHome); + return resolveHookCommand(raw, workspaceRootUri, userHome); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts index 1525bbb59e8..6bdf4afdc89 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts @@ -111,6 +111,18 @@ export function parseCopilotHooks( return result; } +/** + * Result of parsing hooks from a file. + */ +export interface IParseHooksFromFileResult { + readonly format: HookSourceFormat; + readonly hooks: Map; + /** + * Whether all hooks from this file were disabled via `disableAllHooks: true`. + */ + readonly disabledAllHooks: boolean; +} + /** * Parses hooks from any supported format, auto-detecting the format from the file URI. */ @@ -119,22 +131,61 @@ export function parseHooksFromFile( json: unknown, workspaceRootUri: URI | undefined, userHome: string -): { format: HookSourceFormat; hooks: Map } { +): IParseHooksFromFileResult { const format = getHookSourceFormat(fileUri); let hooks: Map; + let disabledAllHooks = false; switch (format) { - case HookSourceFormat.Claude: - hooks = parseClaudeHooks(json, workspaceRootUri, userHome); + case HookSourceFormat.Claude: { + const result = parseClaudeHooks(json, workspaceRootUri, userHome); + hooks = result.hooks; + disabledAllHooks = result.disabledAllHooks; break; + } case HookSourceFormat.Copilot: default: hooks = parseCopilotHooks(json, workspaceRootUri, userHome); break; } - return { format, hooks }; + return { format, hooks, disabledAllHooks }; +} + +/** + * Parses hooks from a file, ignoring the `disableAllHooks` flag. + * Used by diagnostics to show which hooks are hidden when `disableAllHooks: true` is set. + */ +export function parseHooksIgnoringDisableAll( + fileUri: URI, + json: unknown, + workspaceRootUri: URI | undefined, + userHome: string +): IParseHooksFromFileResult { + const format = getHookSourceFormat(fileUri); + + let hooks: Map; + + switch (format) { + case HookSourceFormat.Claude: { + // Strip `disableAllHooks` before parsing so the hooks are still extracted + if (json && typeof json === 'object') { + const { disableAllHooks: _, ...rest } = json as Record; + const result = parseClaudeHooks(rest, workspaceRootUri, userHome); + hooks = result.hooks; + } else { + hooks = new Map(); + } + break; + } + case HookSourceFormat.Copilot: + default: + hooks = parseCopilotHooks(json, workspaceRootUri, userHome); + break; + } + + return { format, hooks, disabledAllHooks: true }; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts index 4d588cc1cfd..8c4d0cbc58a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts @@ -54,8 +54,8 @@ export function getLanguageIdForPromptsType(type: PromptsType): string { case PromptsType.skill: return SKILL_LANGUAGE_ID; case PromptsType.hook: - // Hooks use JSON syntax with schema validation - return 'json'; + // Hooks use JSONC syntax with schema validation + return 'jsonc'; default: throw new Error(`Unknown prompt type: ${type}`); } @@ -71,7 +71,7 @@ export function getPromptsTypeForLanguageId(languageId: string): PromptsType | u return PromptsType.agent; case SKILL_LANGUAGE_ID: return PromptsType.skill; - // Note: hook uses 'json' language ID which is shared, so we don't map it here + // Note: hook uses 'jsonc' language ID which is shared, so we don't map it here default: return undefined; } 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 47b3e64e180..f59d436d4fd 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -273,7 +273,8 @@ export type PromptFileSkipReason = | 'name-mismatch' | 'duplicate-name' | 'parse-error' - | 'disabled'; + | 'disabled' + | 'all-hooks-disabled'; /** * Result of discovering a single prompt file. 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 c8196999288..7d4cb5cc352 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { parse as parseJSONC } from '../../../../../../base/common/json.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ResourceMap, ResourceSet } from '../../../../../../base/common/map.js'; import { basename, dirname, isEqual, joinPath } from '../../../../../../base/common/resources.js'; @@ -1030,10 +1031,16 @@ export class PromptsService extends Disposable implements IPromptsService { for (const hookFile of hookFiles) { try { const content = await this.fileService.readFile(hookFile.uri); - const json = JSON.parse(content.value.toString()); + const json = parseJSONC(content.value.toString()); - // Use format-aware parsing that handles Copilot, Claude, and Cursor formats - const { format, hooks } = parseHooksFromFile(hookFile.uri, json, workspaceRootUri, userHome); + // Use format-aware parsing that handles Copilot and Claude formats + const { format, hooks, disabledAllHooks } = parseHooksFromFile(hookFile.uri, json, workspaceRootUri, userHome); + + // Skip files that have all hooks disabled + if (disabledAllHooks) { + this.logger.trace(`[PromptsService] Skipping hook file with disableAllHooks: ${hookFile.uri}`); + continue; + } for (const [hookType, { hooks: commands }] of hooks) { for (const command of commands) { @@ -1304,6 +1311,14 @@ export class PromptsService extends Disposable implements IPromptsService { private async getHookDiscoveryInfo(token: CancellationToken): Promise { const files: IPromptFileDiscoveryResult[] = []; + // Get user home for tilde expansion + const userHomeUri = await this.pathService.userHome(); + const userHome = userHomeUri.scheme === Schemas.file ? userHomeUri.fsPath : userHomeUri.path; + + // Get workspace root for resolving relative cwd paths + const workspaceFolder = this.workspaceService.getWorkspace().folders[0]; + const workspaceRootUri = workspaceFolder?.uri; + const hookFiles = await this.listPromptFiles(PromptsType.hook, token); for (const promptPath of hookFiles) { const uri = promptPath.uri; @@ -1312,9 +1327,9 @@ export class PromptsService extends Disposable implements IPromptsService { const name = basename(uri); try { - // Try to parse the JSON to validate it + // Try to parse the JSON to validate it (supports JSONC with comments) const content = await this.fileService.readFile(uri); - const json = JSON.parse(content.value.toString()); + const json = parseJSONC(content.value.toString()); // Validate it's an object if (!json || typeof json !== 'object') { @@ -1330,6 +1345,21 @@ export class PromptsService extends Disposable implements IPromptsService { continue; } + // Use format-aware parsing to check for disabledAllHooks + const { disabledAllHooks } = parseHooksFromFile(uri, json, workspaceRootUri, userHome); + + if (disabledAllHooks) { + files.push({ + uri, + storage, + status: 'skipped', + skipReason: 'all-hooks-disabled', + name, + extensionId + }); + continue; + } + // File is valid files.push({ uri, storage, status: 'loaded', name, extensionId }); } catch (e) { diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index d1f19a0f152..d8784ef6dd7 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -7,38 +7,39 @@ 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 { generateUuid } from '../../../../../../base/common/uuid.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 { IChatProgress, IChatService } from '../../chatService/chatService.js'; import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js'; +import { IChatProgress, IChatService } from '../../chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../constants.js'; import { ILanguageModelsService } from '../../languageModels.js'; +import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; +import { IChatAgentRequest, IChatAgentService } from '../../participants/chatAgents.js'; +import { ComputeAutomaticInstructions } from '../../promptSyntax/computeAutomaticInstructions.js'; +import { IChatRequestHooks } from '../../promptSyntax/hookSchema.js'; +import { ICustomAgent, IPromptsService } from '../../promptSyntax/service/promptsService.js'; import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, + isToolSet, 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'; -import { ICustomAgent, IPromptsService } from '../../promptSyntax/service/promptsService.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. @@ -222,6 +223,8 @@ export class RunSubagentTool extends Disposable implements IToolImpl { } else { model.acceptResponseProgress(request, part); } + } else if (part.kind === 'hook') { + model.acceptResponseProgress(request, { ...part, subAgentInvocationId }); } else if (part.kind === 'markdownContent') { if (inEdit) { model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('\n```\n\n') }); @@ -244,6 +247,14 @@ export class RunSubagentTool extends Disposable implements IToolImpl { const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, ChatModeKind.Agent, modeTools, undefined); // agents can not call subagents await computer.collect(variableSet, token); + // Collect hooks from hook .json files + let collectedHooks: IChatRequestHooks | undefined; + try { + collectedHooks = await this.promptsService.getHooks(token); + } catch (error) { + this.logService.warn('[ChatService] Failed to collect hooks:', error); + } + // Build the agent request const agentRequest: IChatAgentRequest = { sessionResource: invocation.context.sessionResource, @@ -258,6 +269,8 @@ export class RunSubagentTool extends Disposable implements IToolImpl { userSelectedTools: modeTools, modeInstructions, parentRequestId: invocation.chatRequestId, + hooks: collectedHooks, + hasHooksEnabled: !!collectedHooks && Object.values(collectedHooks).some(arr => arr.length > 0), }; // Subscribe to tool invocations to clear markdown parts when a tool is invoked 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 index 3b53ed241ba..75d099cd9b7 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -3532,6 +3532,52 @@ suite('LanguageModelToolsService', () => { assert.ok(toolIds.includes('multiSetTool'), 'Tool should be permitted if it belongs to at least one permitted toolset'); }); + test('isPermitted allows internal tools with canBeReferencedInPrompt=false when agent mode is disabled (issue #292935)', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create internal infrastructure tool that explicitly cannot be referenced in prompts + const infrastructureTool: IToolData = { + id: 'infrastructureToolInternal', + toolReferenceName: 'infrastructureToolRef', + modelDescription: 'Infrastructure Tool', + displayName: 'Infrastructure Tool', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: false, + }; + store.add(service.registerToolData(infrastructureTool)); + + // Create internal tool with canBeReferencedInPrompt=true (should be blocked) + const referencableTool: IToolData = { + id: 'referencableTool', + toolReferenceName: 'referencableToolRef', + modelDescription: 'Referencable Tool', + displayName: 'Referencable Tool', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(referencableTool)); + + // Create internal tool with canBeReferencedInPrompt=undefined (should be blocked) + const undefinedTool: IToolData = { + id: 'undefinedTool', + toolReferenceName: 'undefinedToolRef', + modelDescription: 'Undefined Tool', + displayName: 'Undefined Tool', + source: ToolDataSource.Internal, + // canBeReferencedInPrompt is undefined + }; + store.add(service.registerToolData(undefinedTool)); + + // Get tools - only the infrastructure tool should be available + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('infrastructureToolInternal'), 'Internal infrastructure tool with canBeReferencedInPrompt=false should be permitted when agent mode is disabled'); + assert.ok(!toolIds.includes('referencableTool'), 'Internal tool with canBeReferencedInPrompt=true should NOT be permitted when agent mode is disabled'); + assert.ok(!toolIds.includes('undefinedTool'), 'Internal tool with canBeReferencedInPrompt=undefined should NOT be permitted when agent mode is disabled'); + }); + suite('ToolSet when clause filtering (issue #291154)', () => { test('ToolSet.getTools filters tools by when clause', () => { // Create a context key for testing diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatEditsTree.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatEditsTree.test.ts index d9f9a38a76e..82a8283135d 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatEditsTree.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatEditsTree.test.ts @@ -202,7 +202,7 @@ suite('ChatEditsTree', () => { store.dispose(); }); - test('storage listener fires after clear', () => { + test.skip('storage listener fires after clear', () => { // Stub create to avoid DOM/widget side effects in tests let createCallCount = 0; const origCreate = widget.create.bind(widget); @@ -228,7 +228,7 @@ suite('ChatEditsTree', () => { widget.create = origCreate; }); - test('currentSession is updated on rebuild', () => { + test.skip('currentSession is updated on rebuild', () => { // Stub create widget.create = (c, s) => { (widget as unknown as { _currentContainer: HTMLElement | undefined })._currentContainer = c; @@ -244,7 +244,7 @@ suite('ChatEditsTree', () => { assert.strictEqual(widget.currentSession, mockSession); }); - test('setEntries replays after view mode toggle', () => { + test.skip('setEntries replays after view mode toggle', () => { // Stub create and track setEntries calls widget.create = (c, s) => { (widget as unknown as { _currentContainer: HTMLElement | undefined })._currentContainer = c; diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts index 84aa27f5722..6f852ed7285 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts @@ -57,9 +57,10 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - assert.strictEqual(result.size, 1); - assert.ok(result.has(HookType.PreToolUse)); - const entry = result.get(HookType.PreToolUse)!; + assert.strictEqual(result.disabledAllHooks, false); + assert.strictEqual(result.hooks.size, 1); + assert.ok(result.hooks.has(HookType.PreToolUse)); + const entry = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(entry.originalId, 'PreToolUse'); assert.strictEqual(entry.hooks.length, 1); assert.strictEqual(entry.hooks[0].command, 'echo "pre-tool"'); @@ -75,9 +76,9 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - assert.strictEqual(result.size, 2); - assert.ok(result.has(HookType.SessionStart)); - assert.ok(result.has(HookType.Stop)); + assert.strictEqual(result.hooks.size, 2); + assert.ok(result.hooks.has(HookType.SessionStart)); + assert.ok(result.hooks.has(HookType.Stop)); }); test('parses multiple commands for same hook type', () => { @@ -92,13 +93,62 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(entry.hooks.length, 2); assert.strictEqual(entry.hooks[0].command, 'echo "first"'); assert.strictEqual(entry.hooks[1].command, 'echo "second"'); }); }); + suite('disableAllHooks', () => { + test('returns empty hooks and disabledAllHooks=true when disableAllHooks is true', () => { + const json = { + disableAllHooks: true, + hooks: { + PreToolUse: [ + { type: 'command', command: 'echo "should be ignored"' } + ] + } + }; + + const result = parseClaudeHooks(json, workspaceRoot, userHome); + + assert.strictEqual(result.disabledAllHooks, true); + assert.strictEqual(result.hooks.size, 0); + }); + + test('parses hooks normally when disableAllHooks is false', () => { + const json = { + disableAllHooks: false, + hooks: { + PreToolUse: [ + { type: 'command', command: 'echo "should be parsed"' } + ] + } + }; + + const result = parseClaudeHooks(json, workspaceRoot, userHome); + + assert.strictEqual(result.disabledAllHooks, false); + assert.strictEqual(result.hooks.size, 1); + }); + + test('parses hooks normally when disableAllHooks is not present', () => { + const json = { + hooks: { + PreToolUse: [ + { type: 'command', command: 'echo "should be parsed"' } + ] + } + }; + + const result = parseClaudeHooks(json, workspaceRoot, userHome); + + assert.strictEqual(result.disabledAllHooks, false); + assert.strictEqual(result.hooks.size, 1); + }); + }); + suite('nested hooks with matchers', () => { test('parses nested hooks with matcher', () => { const json = { @@ -116,7 +166,7 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(entry.hooks.length, 1); assert.strictEqual(entry.hooks[0].command, 'echo "bash hook"'); }); @@ -138,7 +188,7 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(entry.hooks.length, 2); }); @@ -160,7 +210,7 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(entry.hooks.length, 2); assert.strictEqual(entry.hooks[0].command, 'echo "bash"'); assert.strictEqual(entry.hooks[1].command, 'echo "write"'); @@ -181,55 +231,42 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(entry.hooks.length, 2); assert.strictEqual(entry.hooks[0].command, 'echo "direct"'); assert.strictEqual(entry.hooks[1].command, 'echo "nested"'); }); }); - suite('command without type field', () => { - test('parses command without explicit type field', () => { - const json = { - hooks: { - PreToolUse: [ - { command: 'echo "no type"' } - ] - } - }; - - const result = parseClaudeHooks(json, workspaceRoot, userHome); - - const entry = result.get(HookType.PreToolUse)!; - assert.strictEqual(entry.hooks.length, 1); - assert.strictEqual(entry.hooks[0].command, 'echo "no type"'); - }); - }); - suite('invalid inputs', () => { test('returns empty map for null json', () => { const result = parseClaudeHooks(null, workspaceRoot, userHome); - assert.strictEqual(result.size, 0); + assert.strictEqual(result.hooks.size, 0); + assert.strictEqual(result.disabledAllHooks, false); }); test('returns empty map for undefined json', () => { const result = parseClaudeHooks(undefined, workspaceRoot, userHome); - assert.strictEqual(result.size, 0); + assert.strictEqual(result.hooks.size, 0); + assert.strictEqual(result.disabledAllHooks, false); }); test('returns empty map for non-object json', () => { const result = parseClaudeHooks('string', workspaceRoot, userHome); - assert.strictEqual(result.size, 0); + assert.strictEqual(result.hooks.size, 0); + assert.strictEqual(result.disabledAllHooks, false); }); test('returns empty map for missing hooks property', () => { const result = parseClaudeHooks({}, workspaceRoot, userHome); - assert.strictEqual(result.size, 0); + assert.strictEqual(result.hooks.size, 0); + assert.strictEqual(result.disabledAllHooks, false); }); test('returns empty map for non-object hooks property', () => { const result = parseClaudeHooks({ hooks: 'invalid' }, workspaceRoot, userHome); - assert.strictEqual(result.size, 0); + assert.strictEqual(result.hooks.size, 0); + assert.strictEqual(result.disabledAllHooks, false); }); test('skips unknown hook types', () => { @@ -242,8 +279,8 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - assert.strictEqual(result.size, 1); - assert.ok(result.has(HookType.PreToolUse)); + assert.strictEqual(result.hooks.size, 1); + assert.ok(result.hooks.has(HookType.PreToolUse)); }); test('skips non-array hook entries', () => { @@ -255,7 +292,7 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - assert.strictEqual(result.size, 0); + assert.strictEqual(result.hooks.size, 0); }); test('skips invalid command entries', () => { @@ -271,7 +308,7 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(entry.hooks.length, 1); assert.strictEqual(entry.hooks[0].command, 'valid'); }); @@ -288,7 +325,7 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(entry.hooks.length, 1); assert.strictEqual(entry.hooks[0].command, 'valid'); }); @@ -306,7 +343,7 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.deepStrictEqual(entry.hooks[0].cwd, URI.file('/workspace/src')); }); @@ -321,7 +358,7 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.deepStrictEqual(entry.hooks[0].env, { NODE_ENV: 'production' }); }); @@ -336,9 +373,24 @@ suite('HookClaudeCompat', () => { const result = parseClaudeHooks(json, workspaceRoot, userHome); - const entry = result.get(HookType.PreToolUse)!; + const entry = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(entry.hooks[0].timeout, 60); }); + + test('supports Claude timeout alias', () => { + const json = { + hooks: { + PreToolUse: [ + { type: 'command', command: 'echo "test"', timeout: 1 } + ] + } + }; + + const result = parseClaudeHooks(json, workspaceRoot, userHome); + + const entry = result.hooks.get(HookType.PreToolUse)!; + assert.strictEqual(entry.hooks[0].timeout, 1); + }); }); }); }); @@ -432,9 +484,9 @@ suite('HookSourceFormat', () => { }; const result = parseClaudeHooks(hooksContent, URI.file('/workspace'), '/home/user'); - assert.strictEqual(result.size, 1); - assert.ok(result.has(HookType.PreToolUse)); - const hooks = result.get(HookType.PreToolUse)!; + assert.strictEqual(result.hooks.size, 1); + assert.ok(result.hooks.has(HookType.PreToolUse)); + const hooks = result.hooks.get(HookType.PreToolUse)!; assert.strictEqual(hooks.hooks.length, 1); // Empty command string is falsy and gets omitted by resolveHookCommand assert.strictEqual(hooks.hooks[0].command, undefined); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts new file mode 100644 index 00000000000..7d4ba6ffe51 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { HookType } from '../../../common/promptSyntax/hookSchema.js'; +import { parseCopilotHooks, parseHooksFromFile, HookSourceFormat } from '../../../common/promptSyntax/hookCompatibility.js'; +import { URI } from '../../../../../../base/common/uri.js'; + +suite('HookCompatibility', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('parseCopilotHooks', () => { + const workspaceRoot = URI.file('/workspace'); + const userHome = '/home/user'; + + suite('basic parsing', () => { + test('parses simple hook with command', () => { + const json = { + hooks: { + PreToolUse: [ + { type: 'command', command: 'echo "pre-tool"' } + ] + } + }; + + const result = parseCopilotHooks(json, workspaceRoot, userHome); + + assert.strictEqual(result.size, 1); + assert.ok(result.has(HookType.PreToolUse)); + const entry = result.get(HookType.PreToolUse)!; + assert.strictEqual(entry.hooks.length, 1); + assert.strictEqual(entry.hooks[0].command, 'echo "pre-tool"'); + }); + }); + + suite('invalid inputs', () => { + test('returns empty result for null json', () => { + const result = parseCopilotHooks(null, workspaceRoot, userHome); + assert.strictEqual(result.size, 0); + }); + + test('returns empty result for undefined json', () => { + const result = parseCopilotHooks(undefined, workspaceRoot, userHome); + assert.strictEqual(result.size, 0); + }); + + test('returns empty result for missing hooks property', () => { + const result = parseCopilotHooks({}, workspaceRoot, userHome); + assert.strictEqual(result.size, 0); + }); + }); + }); + + suite('parseHooksFromFile', () => { + const workspaceRoot = URI.file('/workspace'); + const userHome = '/home/user'; + + test('uses Copilot format for .github/hooks/*.json files', () => { + const fileUri = URI.file('/workspace/.github/hooks/my-hooks.json'); + const json = { + hooks: { + PreToolUse: [ + { type: 'command', command: 'echo "test"' } + ] + } + }; + + const result = parseHooksFromFile(fileUri, json, workspaceRoot, userHome); + + assert.strictEqual(result.format, HookSourceFormat.Copilot); + assert.strictEqual(result.disabledAllHooks, false); + assert.strictEqual(result.hooks.size, 1); + }); + + test('uses Claude format for .claude/settings.json files', () => { + const fileUri = URI.file('/workspace/.claude/settings.json'); + const json = { + disableAllHooks: true, + hooks: { + PreToolUse: [ + { type: 'command', command: 'echo "test"' } + ] + } + }; + + const result = parseHooksFromFile(fileUri, json, workspaceRoot, userHome); + + assert.strictEqual(result.format, HookSourceFormat.Claude); + assert.strictEqual(result.disabledAllHooks, true); + assert.strictEqual(result.hooks.size, 0); + }); + + test('disableAllHooks is ignored for Copilot format', () => { + const fileUri = URI.file('/workspace/.github/hooks/hooks.json'); + const json = { + disableAllHooks: true, + hooks: { + SessionStart: [ + { type: 'command', command: 'echo "start"' } + ] + } + }; + + const result = parseHooksFromFile(fileUri, json, workspaceRoot, userHome); + + // Copilot format does not support disableAllHooks + assert.strictEqual(result.disabledAllHooks, false); + assert.strictEqual(result.hooks.size, 1); + }); + + test('disabledAllHooks works for Claude format', () => { + const fileUri = URI.file('/workspace/.claude/settings.local.json'); + const json = { + disableAllHooks: true, + hooks: { + SessionStart: [ + { type: 'command', command: 'echo "start"' } + ] + } + }; + + const result = parseHooksFromFile(fileUri, json, workspaceRoot, userHome); + + assert.strictEqual(result.disabledAllHooks, true); + assert.strictEqual(result.hooks.size, 0); + }); + }); +}); diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index ef940211d69..1e408be694c 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -180,7 +180,7 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut 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 want to allow automatic tasks to run in all trusted workspaces?", + "This workspace has tasks ({0}) defined ({1}) that can launch processes automatically when you open this workspace. Do you want to allow automatic tasks to run in all trusted workspaces?", taskNames.join(', '), Array.from(locations.keys()).join(', ') ), diff --git a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index d9e6583c093..e7cd493ae50 100644 --- a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts @@ -24,7 +24,9 @@ declare module 'vscode' { export function registerChatWorkspaceContextProvider(id: string, provider: ChatWorkspaceContextProvider): Disposable; /** - * Register a chat explicit context provider. Explicit context items are shown as options when the user explicitly attaches context. + * Register a chat explicit context provider. Explicit context items are shown as options when the user explicitly attaches context use the "Attache Context" action in the chat input box. + * + * Explicit context providers should also be statically contributed in package.json using the `chatContext` contribution point. * * To ensure your extension is activated when chat context is requested, make sure to include the `onChatContextProvider:` activation event in your `package.json`. * @@ -109,7 +111,12 @@ declare module 'vscode' { * * @param token A cancellation token. */ - provideChatContext(token: CancellationToken): ProviderResult; + provideWorkspaceChatContext(token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + provideChatContext?(token: CancellationToken): ProviderResult; } export interface ChatExplicitContextProvider { @@ -121,7 +128,12 @@ declare module 'vscode' { * * @param token A cancellation token. */ - provideChatContext(token: CancellationToken): ProviderResult; + provideExplicitChatContext(token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + provideChatContext?(token: CancellationToken): ProviderResult; /** * If a chat context item is provided without a `value`, this method is called to resolve the `value` for the item. @@ -129,7 +141,12 @@ declare module 'vscode' { * @param context The context item to resolve. * @param token A cancellation token. */ - resolveChatContext(context: T, token: CancellationToken): ProviderResult; + resolveExplicitChatContext(context: T, token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + resolveChatContext?(context: T, token: CancellationToken): ProviderResult; } export interface ChatResourceContextProvider { @@ -144,7 +161,12 @@ declare module 'vscode' { * @param options Options include the resource for which to provide context. * @param token A cancellation token. */ - provideChatContext(options: { resource: Uri }, token: CancellationToken): ProviderResult; + provideResourceChatContext(options: { resource: Uri }, token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + provideChatContext?(options: { resource: Uri }, token: CancellationToken): ProviderResult; /** * If a chat context item is provided without a `value`, this method is called to resolve the `value` for the item. @@ -152,7 +174,12 @@ declare module 'vscode' { * @param context The context item to resolve. * @param token A cancellation token. */ - resolveChatContext(context: T, token: CancellationToken): ProviderResult; + resolveResourceChatContext(context: T, token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + resolveChatContext?(context: T, token: CancellationToken): ProviderResult; } /** @@ -168,19 +195,19 @@ declare module 'vscode' { /** * Provide a list of chat context items to be included as workspace context for all chat requests. - * @deprecated Use {@link ChatWorkspaceContextProvider.provideChatContext} instead. + * @deprecated Use {@link ChatWorkspaceContextProvider.provideWorkspaceChatContext} instead. */ provideWorkspaceChatContext?(token: CancellationToken): ProviderResult; /** * Provide a list of chat context items that a user can choose from. - * @deprecated Use {@link ChatExplicitContextProvider.provideChatContext} instead. + * @deprecated Use {@link ChatExplicitContextProvider.provideExplicitChatContext} instead. */ provideChatContextExplicit?(token: CancellationToken): ProviderResult; /** * Given a particular resource, provide a chat context item for it. - * @deprecated Use {@link ChatResourceContextProvider.provideChatContext} instead. + * @deprecated Use {@link ChatResourceContextProvider.provideResourceChatContext} instead. */ provideChatContextForResource?(options: { resource: Uri }, token: CancellationToken): ProviderResult;