From 7c6cca7b14427b588e97048b9a8953cbdbebce54 Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 12 Feb 2026 12:08:56 +0100 Subject: [PATCH 01/12] Add native private field to property transform for esbuild bundles --- build/gulpfile.vscode.ts | 1 + build/gulpfile.vscode.web.ts | 1 + build/next/index.ts | 32 ++- build/next/private-to-property.ts | 191 ++++++++++++++ build/next/test/private-to-property.test.ts | 275 ++++++++++++++++++++ build/package.json | 2 +- 6 files changed, 498 insertions(+), 4 deletions(-) create mode 100644 build/next/private-to-property.ts create mode 100644 build/next/test/private-to-property.test.ts diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index f6e4e7afe49fb..a103f116c1e6d 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -187,6 +187,7 @@ function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, target: const args = [scriptPath, 'bundle', '--out', outDir, '--target', target]; if (minify) { args.push('--minify'); + args.push('--mangle-privates'); } if (nls) { args.push('--nls'); diff --git a/build/gulpfile.vscode.web.ts b/build/gulpfile.vscode.web.ts index cdaf57000fac1..e9cc3720fcf7f 100644 --- a/build/gulpfile.vscode.web.ts +++ b/build/gulpfile.vscode.web.ts @@ -39,6 +39,7 @@ function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean): Promis const args = [scriptPath, 'bundle', '--out', outDir, '--target', 'web']; if (minify) { args.push('--minify'); + args.push('--mangle-privates'); } if (nls) { args.push('--nls'); diff --git a/build/next/index.ts b/build/next/index.ts index 2535364d64e6c..8fded5ef748bb 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -10,6 +10,7 @@ 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 { convertPrivateFields, type ConvertPrivateFieldsResult } from './private-to-property.ts'; import { getVersion } from '../lib/getVersion.ts'; import product from '../../product.json' with { type: 'json' }; import packageJson from '../../package.json' with { type: 'json' }; @@ -41,6 +42,7 @@ const options = { watch: process.argv.includes('--watch'), minify: process.argv.includes('--minify'), nls: process.argv.includes('--nls'), + manglePrivates: process.argv.includes('--mangle-privates'), excludeTests: process.argv.includes('--exclude-tests'), out: getArgValue('--out'), target: getArgValue('--target') ?? 'desktop', // 'desktop' | 'server' | 'server-web' | 'web' @@ -740,7 +742,7 @@ async function transpile(outDir: string, excludeTests: boolean): Promise { // Bundle (Goal 2: JS → bundled JS) // ============================================================================ -async function bundle(outDir: string, doMinify: boolean, doNls: boolean, target: BuildTarget, sourceMapBaseUrl?: string): Promise { +async function bundle(outDir: string, doMinify: boolean, doNls: boolean, doManglePrivates: boolean, target: BuildTarget, sourceMapBaseUrl?: string): Promise { await cleanDir(outDir); // Write build date file (used by packaging to embed in product.json) @@ -748,7 +750,7 @@ async function bundle(outDir: string, doMinify: boolean, doNls: boolean, target: 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)' : ''}`); + console.log(`[bundle] ${SRC_DIR} → ${outDir} (target: ${target})${doMinify ? ' (minify)' : ''}${doNls ? ' (nls)' : ''}${doManglePrivates ? ' (mangle-privates)' : ''}`); const t1 = Date.now(); // Read TSLib for banner @@ -902,6 +904,7 @@ ${tslib}`, // Post-process and write all output files let bundled = 0; + const mangleStats: { file: string; result: ConvertPrivateFieldsResult }[] = []; for (const { result } of buildResults) { if (!result.outputFiles) { continue; @@ -918,6 +921,15 @@ ${tslib}`, content = postProcessNLS(content, indexMap, preserveEnglish); } + // Convert native #private fields to regular properties + if (file.path.endsWith('.js') && doManglePrivates) { + const mangleResult = convertPrivateFields(content, file.path); + content = mangleResult.code; + if (mangleResult.editCount > 0) { + mangleStats.push({ file: path.relative(path.join(REPO_ROOT, outDir), file.path), result: mangleResult }); + } + } + // Rewrite sourceMappingURL to CDN URL if configured if (sourceMapBaseUrl) { const relativePath = path.relative(path.join(REPO_ROOT, outDir), file.path); @@ -940,6 +952,19 @@ ${tslib}`, bundled++; } + // Log mangle-privates stats + if (doManglePrivates && mangleStats.length > 0) { + let totalClasses = 0, totalFields = 0, totalEdits = 0, totalElapsed = 0; + for (const { file, result } of mangleStats) { + console.log(`[mangle-privates] ${file}: ${result.classCount} classes, ${result.fieldCount} fields, ${result.editCount} edits, ${result.elapsed}ms`); + totalClasses += result.classCount; + totalFields += result.fieldCount; + totalEdits += result.editCount; + totalElapsed += result.elapsed; + } + console.log(`[mangle-privates] Total: ${totalClasses} classes, ${totalFields} fields, ${totalEdits} edits, ${totalElapsed}ms`); + } + // Copy resources (exclude dev files and tests for production) await copyResources(outDir, target, true, true); @@ -1075,6 +1100,7 @@ Options for 'transpile': Options for 'bundle': --minify Minify the output bundles --nls Process NLS (localization) strings + --mangle-privates Convert native #private fields to regular properties --out Output directory (default: out-vscode) --target Build target: desktop (default), server, server-web, web --source-map-base-url Rewrite sourceMappingURL to CDN URL @@ -1118,7 +1144,7 @@ async function main(): Promise { break; case 'bundle': - await bundle(options.out ?? OUT_VSCODE_DIR, options.minify, options.nls, options.target as BuildTarget, options.sourceMapBaseUrl); + await bundle(options.out ?? OUT_VSCODE_DIR, options.minify, options.nls, options.manglePrivates, options.target as BuildTarget, options.sourceMapBaseUrl); break; default: diff --git a/build/next/private-to-property.ts b/build/next/private-to-property.ts new file mode 100644 index 0000000000000..64f1c4e74bf97 --- /dev/null +++ b/build/next/private-to-property.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from 'typescript'; + +/** + * Converts native ES private fields (`#foo`) into regular JavaScript properties with short, + * globally unique names (e.g., `$a`, `$b`). This achieves two goals: + * + * 1. **Performance**: Native private fields are slower than regular properties in V8. + * 2. **Mangling**: Short replacement names reduce bundle size. + * + * ## Why not simply strip `#`? + * + * - **Inheritance collision**: If `class B extends A` and both declare `#x`, stripping `#` + * yields `x` on both - collision on child instances. + * - **Public property shadowing**: `class Foo extends Error { static #name = ... }` - stripping + * `#` produces `name` which shadows `Error.name`. + * + * ## Strategy: Globally unique names with `$` prefix + * + * Each (class, privateFieldName) pair gets a unique name from a global counter: `$a`, `$b`, ... + * This guarantees no inheritance collision and no shadowing of public properties. + * + * ## Why this is safe with syntax-only analysis + * + * Native `#` fields are **lexically scoped** to their declaring class body. Every declaration + * and every usage site is syntactically inside the class body. A single AST walk is sufficient + * to find all sites - no cross-file analysis or type checker needed. + */ + +// Short name generator: $a, $b, ..., $z, $A, ..., $Z, $aa, $ab, ... +const CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +function generateShortName(index: number): string { + let name = ''; + do { + name = CHARS[index % CHARS.length] + name; + index = Math.floor(index / CHARS.length) - 1; + } while (index >= 0); + return '$' + name; +} + +interface Edit { + start: number; + end: number; + newText: string; +} + +// Private name → replacement name per class (identified by position in file) +type ClassScope = Map; + +export interface ConvertPrivateFieldsResult { + readonly code: string; + readonly classCount: number; + readonly fieldCount: number; + readonly editCount: number; + readonly elapsed: number; +} + +/** + * Converts all native `#` private fields/methods in the given JavaScript source to regular + * properties with short, globally unique names. + * + * @param code The JavaScript source code (typically a bundled output file). + * @param filename Used for TypeScript parser diagnostics only. + * @returns The transformed source code with `#` fields replaced, plus stats. + */ +export function convertPrivateFields(code: string, filename: string): ConvertPrivateFieldsResult { + const t1 = Date.now(); + // Quick bail-out: if there are no `#` characters, nothing to do + if (!code.includes('#')) { + return { code, classCount: 0, fieldCount: 0, editCount: 0, elapsed: Date.now() - t1 }; + } + + const sourceFile = ts.createSourceFile(filename, code, ts.ScriptTarget.ESNext, false, ts.ScriptKind.JS); + + // Global counter for unique name generation + let nameCounter = 0; + let classCount = 0; + + // Collect all edits + const edits: Edit[] = []; + + // Class stack for resolving private names in nested classes. + // When a PrivateIdentifier is encountered, we search from innermost to outermost + // class scope - matching JS lexical resolution semantics. + const classStack: ClassScope[] = []; + + visit(sourceFile); + + if (edits.length === 0) { + return { code, classCount: 0, fieldCount: 0, editCount: 0, elapsed: Date.now() - t1 }; + } + + // Apply edits using substring concatenation (O(N+K), not O(N*K) like char-array splice) + edits.sort((a, b) => a.start - b.start); + const parts: string[] = []; + let lastEnd = 0; + for (const edit of edits) { + parts.push(code.substring(lastEnd, edit.start)); + parts.push(edit.newText); + lastEnd = edit.end; + } + parts.push(code.substring(lastEnd)); + return { code: parts.join(''), classCount, fieldCount: nameCounter, editCount: edits.length, elapsed: Date.now() - t1 }; + + // --- AST walking --- + + function visit(node: ts.Node): void { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + visitClass(node); + return; + } + ts.forEachChild(node, visit); + } + + function visitClass(node: ts.ClassDeclaration | ts.ClassExpression): void { + // 1) Collect all private field/method/accessor declarations in THIS class + const scope: ClassScope = new Map(); + for (const member of node.members) { + if (member.name && ts.isPrivateIdentifier(member.name)) { + const name = member.name.text; + if (!scope.has(name)) { + scope.set(name, generateShortName(nameCounter++)); + } + } + } + + if (scope.size > 0) { + classCount++; + } + classStack.push(scope); + + // 2) Walk the class body, replacing PrivateIdentifier nodes + ts.forEachChild(node, function walkInClass(child: ts.Node): void { + // Nested class: process independently with its own scope + if ((ts.isClassDeclaration(child) || ts.isClassExpression(child)) && child !== node) { + visitClass(child); + return; + } + + // Handle `#field in expr` (ergonomic brand check) - needs string literal replacement + if (ts.isBinaryExpression(child) && + child.operatorToken.kind === ts.SyntaxKind.InKeyword && + ts.isPrivateIdentifier(child.left)) { + const resolved = resolvePrivateName(child.left.text); + if (resolved !== undefined) { + edits.push({ + start: child.left.getStart(sourceFile), + end: child.left.getEnd(), + newText: `'${resolved}'` + }); + } + // Still need to walk the right-hand side for any private field usages + ts.forEachChild(child.right, walkInClass); + return; + } + + // Normal PrivateIdentifier usage (declaration, property access, method call) + if (ts.isPrivateIdentifier(child)) { + const resolved = resolvePrivateName(child.text); + if (resolved !== undefined) { + edits.push({ + start: child.getStart(sourceFile), + end: child.getEnd(), + newText: resolved + }); + } + return; + } + + ts.forEachChild(child, walkInClass); + }); + + classStack.pop(); + } + + function resolvePrivateName(name: string): string | undefined { + // Walk from innermost to outermost class scope (matches JS lexical resolution) + for (let i = classStack.length - 1; i >= 0; i--) { + const resolved = classStack[i].get(name); + if (resolved !== undefined) { + return resolved; + } + } + return undefined; + } +} diff --git a/build/next/test/private-to-property.test.ts b/build/next/test/private-to-property.test.ts new file mode 100644 index 0000000000000..3cde63a4bdaf4 --- /dev/null +++ b/build/next/test/private-to-property.test.ts @@ -0,0 +1,275 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { convertPrivateFields } from '../private-to-property.ts'; + +suite('convertPrivateFields', () => { + + test('no # characters — quick bail-out', () => { + const result = convertPrivateFields('const x = 1; function foo() { return x; }', 'test.js'); + assert.strictEqual(result.code, 'const x = 1; function foo() { return x; }'); + assert.strictEqual(result.editCount, 0); + assert.strictEqual(result.classCount, 0); + assert.strictEqual(result.fieldCount, 0); + }); + + test('class without private fields — identity', () => { + const code = 'class Plain { x = 1; get() { return this.x; } }'; + const result = convertPrivateFields(code, 'test.js'); + assert.strictEqual(result.code, code); + assert.strictEqual(result.editCount, 0); + }); + + test('basic private field', () => { + const code = 'class Foo { #x = 1; get() { return this.#x; } }'; + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#x'), 'should not contain #x'); + assert.ok(result.code.includes('$a'), 'should contain replacement $a'); + assert.strictEqual(result.classCount, 1); + assert.strictEqual(result.fieldCount, 1); + assert.strictEqual(result.editCount, 2); + }); + + test('multiple private fields in one class', () => { + const code = 'class Foo { #x = 1; #y = 2; get() { return this.#x + this.#y; } }'; + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#x')); + assert.ok(!result.code.includes('#y')); + assert.strictEqual(result.fieldCount, 2); + assert.strictEqual(result.editCount, 4); + }); + + test('inheritance — same private name in parent and child get different replacements', () => { + const code = [ + 'class Parent { #a = 1; getA() { return this.#a; } }', + 'class Child extends Parent { #a = 2; getChildA() { return this.#a; } }', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#a')); + assert.ok(result.code.includes('$a'), 'Parent should get $a'); + assert.ok(result.code.includes('$b'), 'Child should get $b'); + }); + + test('static private field — no clash with inherited public property', () => { + const code = [ + 'class MyError extends Error {', + ' static #name = "MyError";', + ' check(data) { return data.name !== MyError.#name; }', + '}', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#name')); + assert.ok(result.code.includes('$a')); + assert.ok(result.code.includes('data.name'), 'public property should be preserved'); + }); + + test('private method', () => { + const code = [ + 'class Bar {', + ' #normalize(s) { return s.toLowerCase(); }', + ' process(s) { return this.#normalize(s); }', + '}', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#normalize')); + assert.strictEqual(result.fieldCount, 1); + }); + + test('getter/setter pair', () => { + const code = [ + 'class WithAccessors {', + ' #_val;', + ' get #val() { return this.#_val; }', + ' set #val(v) { this.#_val = v; }', + ' init() { this.#val = 42; }', + '}', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#_val')); + assert.ok(!result.code.includes('#val')); + assert.strictEqual(result.fieldCount, 2); + }); + + test('nested classes — separate scopes', () => { + const code = [ + 'class Outer {', + ' #x = 1;', + ' method() {', + ' class Inner {', + ' #y = 2;', + ' foo() { return this.#y; }', + ' }', + ' return this.#x;', + ' }', + '}', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#x')); + assert.ok(!result.code.includes('#y')); + assert.strictEqual(result.classCount, 2); + }); + + test('nested class accessing outer private field', () => { + const code = [ + 'class Outer {', + ' #x = 1;', + ' method() {', + ' class Inner {', + ' foo(o) { return o.#x; }', + ' }', + ' return this.#x;', + ' }', + '}', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#x')); + const matches = result.code.match(/\$a/g); + assert.strictEqual(matches?.length, 3, 'decl + this.#x + o.#x = 3'); + }); + + test('nested classes — same private name get different replacements', () => { + const code = [ + 'class Outer {', + ' #x = 1;', + ' m() {', + ' class Inner {', + ' #x = 2;', + ' f() { return this.#x; }', + ' }', + ' return this.#x;', + ' }', + '}', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#x')); + assert.ok(result.code.includes('$a'), 'Outer.#x → $a'); + assert.ok(result.code.includes('$b'), 'Inner.#x → $b'); + }); + + test('unrelated classes with same private name', () => { + const code = [ + 'class A { #data = 1; get() { return this.#data; } }', + 'class B { #data = 2; get() { return this.#data; } }', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#data')); + assert.ok(result.code.includes('$a')); + assert.ok(result.code.includes('$b')); + }); + + test('cross-instance access', () => { + const code = [ + 'class Foo {', + ' #secret = 42;', + ' equals(other) { return this.#secret === other.#secret; }', + '}', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#secret')); + const matches = result.code.match(/\$a/g); + assert.strictEqual(matches?.length, 3); + }); + + test('string containing # is not modified', () => { + const code = [ + 'class Foo {', + ' #x = 1;', + ' label = "use #x for private";', + ' get() { return this.#x; }', + '}', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.ok(result.code.includes('"use #x for private"'), 'string preserved'); + assert.ok(!result.code.includes('this.#x'), 'usage replaced'); + }); + + test('#field in expr — brand check uses quoted string', () => { + const code = 'class Foo { #brand; static check(x) { if (#brand in x) return true; } }'; + const result = convertPrivateFields(code, 'test.js'); + assert.ok(!result.code.includes('#brand')); + assert.ok(result.code.includes('\'$a\' in x'), 'quoted string for in-check'); + }); + + test('string #brand in obj is not treated as private field', () => { + const code = 'class Foo { #brand = true; isFoo(obj) { return "#brand" in obj; } }'; + const result = convertPrivateFields(code, 'test.js'); + assert.ok(result.code.includes('"#brand" in obj'), 'string literal preserved'); + }); + + test('transformed code is valid JavaScript', () => { + const code = [ + 'class Base { #id = 0; getId() { return this.#id; } }', + 'class Derived extends Base { #name; constructor(n) { super(); this.#name = n; } getName() { return this.#name; } }', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.doesNotThrow(() => new Function(result.code)); + }); + + test('transformed code executes correctly', () => { + const code = [ + 'class Counter {', + ' #count = 0;', + ' increment() { this.#count++; }', + ' get value() { return this.#count; }', + '}', + 'const c = new Counter();', + 'c.increment(); c.increment(); c.increment();', + 'return c.value;', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.strictEqual(new Function(result.code)(), 3); + }); + + test('transformed code executes correctly with inheritance', () => { + const code = [ + 'class Animal {', + ' #sound;', + ' constructor(s) { this.#sound = s; }', + ' speak() { return this.#sound; }', + '}', + 'class Dog extends Animal {', + ' #tricks = [];', + ' constructor() { super("woof"); }', + ' learn(trick) { this.#tricks.push(trick); }', + ' show() { return this.#tricks.join(","); }', + '}', + 'const d = new Dog();', + 'd.learn("sit"); d.learn("shake");', + 'return d.speak() + ":" + d.show();', + ].join('\n'); + const result = convertPrivateFields(code, 'test.js'); + assert.strictEqual(new Function(result.code)(), 'woof:sit,shake'); + }); + + suite('name generation', () => { + + test('generates $a through $Z for 52 fields', () => { + const fields = []; + const usages = []; + for (let i = 0; i < 52; i++) { + fields.push(`#f${i};`); + usages.push(`this.#f${i}`); + } + const code = `class Big { ${fields.join(' ')} get() { return ${usages.join(' + ')}; } }`; + const result = convertPrivateFields(code, 'test.js'); + assert.ok(result.code.includes('$a')); + assert.ok(result.code.includes('$Z')); + assert.strictEqual(result.fieldCount, 52); + }); + + test('wraps to $aa after $Z', () => { + const fields = []; + const usages = []; + for (let i = 0; i < 53; i++) { + fields.push(`#f${i};`); + usages.push(`this.#f${i}`); + } + const code = `class Big { ${fields.join(' ')} get() { return ${usages.join(' + ')}; } }`; + const result = convertPrivateFields(code, 'test.js'); + assert.ok(result.code.includes('$aa')); + }); + }); +}); diff --git a/build/package.json b/build/package.json index b6ccfdfc302dc..e889b6ac54d7b 100644 --- a/build/package.json +++ b/build/package.json @@ -70,7 +70,7 @@ "pretypecheck": "npm run copy-policy-dto", "typecheck": "cd .. && npx tsgo --project build/tsconfig.json", "watch": "npm run typecheck -- --watch", - "test": "mocha --ui tdd 'lib/**/*.test.ts'" + "test": "mocha --ui tdd '{lib,next}/**/*.test.ts'" }, "optionalDependencies": { "tree-sitter-typescript": "^0.23.2", From bccf22cb8d8cfef91bb899d25c7a688beb50661f Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 13 Feb 2026 14:59:16 +0100 Subject: [PATCH 02/12] Exclude --- build/next/index.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/build/next/index.ts b/build/next/index.ts index 8fded5ef748bb..21a6aa99f8a86 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -63,6 +63,17 @@ const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]); // Entry Points (from build/buildfile.ts) // ============================================================================ +// Extension host bundles are excluded from private field mangling because they +// expose API surface to extensions where encapsulation matters. +const extensionHostEntryPoints = [ + 'vs/workbench/api/node/extensionHostProcess', + 'vs/workbench/api/worker/extensionHostWorkerMain', +]; + +function isExtensionHostBundle(filePath: string): boolean { + return extensionHostEntryPoints.some(ep => filePath.endsWith(`${ep}.js`)); +} + // Workers - shared between targets const workerEntryPoints = [ 'vs/editor/common/services/editorWebWorkerMain', @@ -921,8 +932,10 @@ ${tslib}`, content = postProcessNLS(content, indexMap, preserveEnglish); } - // Convert native #private fields to regular properties - if (file.path.endsWith('.js') && doManglePrivates) { + // Convert native #private fields to regular properties. + // Skip extension host bundles - they expose API surface to extensions + // where true encapsulation matters more than the perf gain. + if (file.path.endsWith('.js') && doManglePrivates && !isExtensionHostBundle(file.path)) { const mangleResult = convertPrivateFields(content, file.path); content = mangleResult.code; if (mangleResult.editCount > 0) { From 814a9ce765445a004810bd4331f303349fd5dd70 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 13 Feb 2026 15:38:39 +0100 Subject: [PATCH 03/12] Refactor resource copying for transpile and bundle builds - Implemented copyAllNonTsFiles() for transpile/dev builds to copy all non-TS files from src/ to output, aligning with previous gulp behavior. - Retained copyResources() for production builds using curated resource patterns. - Removed unnecessary devOnlyResourcePatterns and testFixturePatterns. - Updated watch mode to accept any non-TS file changes. --- build/next/index.ts | 145 ++++++++++++++---------------------------- build/next/working.md | 14 ++++ 2 files changed, 60 insertions(+), 99 deletions(-) diff --git a/build/next/index.ts b/build/next/index.ts index 21a6aa99f8a86..73a08933db167 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -229,18 +229,6 @@ const commonResourcePatterns = [ 'vs/workbench/browser/parts/editor/media/letterpress*.svg', ]; -// 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, @@ -368,28 +356,6 @@ function getResourcePatternsForTarget(target: BuildTarget): string[] { } } -// 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 // ============================================================================ @@ -511,34 +477,57 @@ async function compileStandaloneFiles(outDir: string, doMinify: boolean, target: 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', { +/** + * Copy ALL non-TypeScript files from src/ to the output directory. + * This matches the old gulp build behavior where `gulp.src('src/**')` streams + * every file and non-TS files bypass the compiler via tsFilter.restore. + * Used for development/transpile builds only - production bundles use + * copyResources() with curated per-target patterns instead. + */ +async function copyAllNonTsFiles(outDir: string, excludeTests: boolean): Promise { + console.log(`[resources] Copying all non-TS files to ${outDir}...`); + + const ignorePatterns = [ + // Exclude .ts files but keep .d.ts files (they're needed at runtime for type references) + '**/*.ts', + ]; + if (excludeTests) { + ignorePatterns.push('**/test/**'); + } + + const files = await globAsync('**/*', { + cwd: path.join(REPO_ROOT, SRC_DIR), + nodir: true, + ignore: ignorePatterns, + }); + + // Re-include .d.ts files that were excluded by the *.ts ignore + const dtsFiles = await globAsync('**/*.d.ts', { cwd: path.join(REPO_ROOT, SRC_DIR), ignore: excludeTests ? ['**/test/**'] : [], }); - for (const file of cssFiles) { + const allFiles = [...new Set([...files, ...dtsFiles])]; + + await Promise.all(allFiles.map(file => { const srcPath = path.join(REPO_ROOT, SRC_DIR, file); const destPath = path.join(REPO_ROOT, outDir, file); + return copyFile(srcPath, destPath); + })); - await copyFile(srcPath, destPath); - } - - return cssFiles.length; + console.log(`[resources] Copied ${allFiles.length} files`); } -async function copyResources(outDir: string, target: BuildTarget, excludeDevFiles = false, excludeTests = false): Promise { +/** + * Copy curated resource files for production bundles. + * Uses specific per-target patterns matching the old build's vscodeResourceIncludes, + * serverResourceIncludes, etc. Only called by bundle() - transpile uses copyAllNonTsFiles(). + */ +async function copyResources(outDir: string, target: BuildTarget): 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 ignorePatterns = ['**/test/**', '**/*-dev.html']; const resourcePatterns = getResourcePatternsForTarget(target); for (const pattern of resourcePatterns) { @@ -556,47 +545,7 @@ async function copyResources(outDir: string, target: BuildTarget, excludeDevFile } } - // 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)`); - } + console.log(`[resources] Copied ${copied} files`); } // ============================================================================ @@ -978,8 +927,8 @@ ${tslib}`, console.log(`[mangle-privates] Total: ${totalClasses} classes, ${totalFields} fields, ${totalEdits} edits, ${totalElapsed}ms`); } - // Copy resources (exclude dev files and tests for production) - await copyResources(outDir, target, true, true); + // Copy resources (curated per-target patterns for production) + await copyResources(outDir, target); // Compile standalone TypeScript files (like Electron preload scripts) that cannot be bundled await compileStandaloneFiles(outDir, doMinify, target); @@ -1012,7 +961,7 @@ async function watch(): Promise { const t1 = Date.now(); try { await transpile(outDir, false); - await copyResources(outDir, 'desktop', false, false); + await copyAllNonTsFiles(outDir, false); console.log(`Finished transpilation with 0 errors after ${Date.now() - t1} ms`); } catch (err) { console.error('[watch] Initial build failed:', err); @@ -1063,9 +1012,6 @@ async function watch(): Promise { } }; - // 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); @@ -1074,7 +1020,8 @@ async function watch(): Promise { 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))) { + } else { + // Copy any non-TS file (matches old gulp build's `src/**` behavior) pendingCopyFiles.add(file.path); } @@ -1151,7 +1098,7 @@ async function main(): Promise { 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); + await copyAllNonTsFiles(outDir, options.excludeTests); console.log(`[transpile] Done in ${Date.now() - t1}ms`); } break; diff --git a/build/next/working.md b/build/next/working.md index bbf23a99806b1..71aac3fbdaf4c 100644 --- a/build/next/working.md +++ b/build/next/working.md @@ -97,6 +97,20 @@ Two placeholders that need injection: **Lesson:** Don't add new output file formats that create parity differences with the old build. The old build is the reference. +### 7. Resource Copying: Transpile vs Bundle + +**Problem:** The new build used curated, specific resource pattern lists (e.g., `desktopResourcePatterns`) for **both** transpile/dev and production/bundle builds. Team members kept discovering missing resources because every new non-TS file in `src/` required manually adding its pattern. + +**Root cause:** The old gulp build uses `gulp.src('src/**')` for dev/transpile — a catch-all glob that streams **every file** in `src/`. Non-TS files bypass the compiler via `tsFilter` + `tsFilter.restore` and land in `out/` untouched. This is inherently complete. The old build only uses curated resource lists for **production packaging** (`vscodeResourceIncludes`, `serverResourceIncludes` in the gulpfiles). + +**Fix:** +- **Transpile/dev path** (`transpile` command, `--watch` mode): Now uses `copyAllNonTsFiles()` which copies ALL non-TS files from `src/` to the output, matching old `gulp.src('src/**')` behavior. No curated patterns needed. +- **Bundle/production path** (`bundle` command): Continues using `copyResources()` with curated per-target patterns, matching old `vscodeResourceIncludes` etc. +- Removed `devOnlyResourcePatterns` and `testFixturePatterns` — no longer needed since the broad copy handles all dev resources. +- Watch mode incremental copy now accepts **any** non-`.ts` file change (removed the `copyExtensions` allowlist). + +**Lesson:** Dev builds should copy everything (completeness matters); production builds should be selective (size matters). Don't mix the two strategies. + --- ## Testing the Fix From a1caba83a8f795375c1113b62374024c801b98c6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 16 Feb 2026 15:38:20 +1100 Subject: [PATCH 04/12] Filter selectable background agent models (#295501) --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index a37c58d3723be..a6493ae2c62d7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -1069,10 +1069,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (sessionType) { // Session has a specific chat session type - show only models that target // this session type, if any such models exist. - const targeted = models.filter(entry => entry.metadata?.targetChatSessionType === sessionType); - if (targeted.length > 0) { - return targeted; - } + return models.filter(entry => entry.metadata?.targetChatSessionType === sessionType && entry.metadata?.isUserSelectable); } // No session type or no targeted models - show general models (those without From 0235281e5611806f638bc56521c8f677c521bc6c Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 16 Feb 2026 17:06:49 +1100 Subject: [PATCH 05/12] Support extension contributed custom agents for background agents (#295517) --- .../chat/browser/widget/input/modePickerActionItem.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 071230853fa71..363704772e72f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -176,20 +176,13 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { }; }; - const isUserDefinedCustomAgent = (mode: IChatMode): boolean => { - if (mode.isBuiltin || !mode.source) { - return false; - } - return mode.source.storage === PromptsStorage.local || mode.source.storage === PromptsStorage.user; - }; - const actionProviderWithCustomAgentTarget: IActionWidgetDropdownActionProvider = { getActions: () => { const modes = chatModeService.getModes(); const currentMode = delegate.currentMode.get(); const filteredCustomModes = modes.custom.filter(mode => { const target = mode.target.get(); - return isUserDefinedCustomAgent(mode) && (target === customAgentTarget); + return target === customAgentTarget || target === Target.Undefined; }); // Always include the default "Agent" option first const checked = currentMode.id === ChatMode.Agent.id; From f019353b36a9992c238b32bbf170e7395219d86b Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 16 Feb 2026 06:54:19 +0000 Subject: [PATCH 06/12] Copilot-aided disposable fixes (#295502) * Copilot-aided disposable fixes * Undo this, leave it for later * Undo making some classes disposable when those classes weren't properly tracked --- src/vs/base/browser/ui/grid/gridview.ts | 3 +++ src/vs/base/browser/ui/list/listView.ts | 4 ++-- src/vs/base/browser/ui/table/tableWidget.ts | 2 +- src/vs/base/browser/ui/tree/abstractTree.ts | 6 +++--- src/vs/base/browser/ui/tree/asyncDataTree.ts | 2 ++ src/vs/base/parts/ipc/common/ipc.net.ts | 4 ++-- src/vs/base/parts/ipc/common/ipc.ts | 1 + .../colorPicker/browser/colorPickerModel.ts | 10 ++++++---- .../platform/debug/common/extensionHostDebugIpc.ts | 10 +++++----- src/vs/platform/tunnel/common/tunnel.ts | 1 + src/vs/platform/update/common/updateIpc.ts | 2 +- src/vs/server/node/extensionHostConnection.ts | 2 +- src/vs/server/node/remoteExtensionManagement.ts | 1 + src/vs/workbench/api/browser/mainThreadComments.ts | 3 +++ src/vs/workbench/api/browser/mainThreadEditor.ts | 1 + .../api/browser/mainThreadLanguageFeatures.ts | 4 ++-- .../api/browser/mainThreadLanguageModels.ts | 8 +++++++- src/vs/workbench/api/browser/mainThreadSCM.ts | 2 ++ .../workbench/api/common/extHostAuthentication.ts | 1 + src/vs/workbench/api/common/extHostChatAgents2.ts | 2 ++ src/vs/workbench/api/common/extHostComments.ts | 1 + src/vs/workbench/api/common/extHostDocuments.ts | 14 +++++++------- .../workbench/api/common/extHostNotebookKernels.ts | 4 ++-- src/vs/workbench/api/common/extHostSCM.ts | 3 +++ src/vs/workbench/api/common/extHostTelemetry.ts | 1 + .../workbench/api/common/extHostTerminalService.ts | 2 +- .../api/common/extHostTerminalShellIntegration.ts | 6 +++--- src/vs/workbench/api/common/extHostTextEditors.ts | 14 +++++++------- .../workbench/api/common/extHostTunnelService.ts | 2 +- .../browser/chatManagement/chatModelsWidget.ts | 7 ++++++- .../chatContentParts/chatChangesSummaryPart.ts | 8 ++++---- .../widget/chatContentParts/codeBlockPart.ts | 2 +- .../comments/browser/commentThreadZoneWidget.ts | 2 +- .../browser/editSessionsStorageService.ts | 4 ++-- .../workbench/contrib/scm/browser/scmViewPane.ts | 12 ++++++------ .../terminalContrib/links/browser/terminalLink.ts | 2 +- .../typeAhead/browser/terminalTypeAheadAddon.ts | 12 ++++++------ .../browser/gettingStartedService.ts | 8 ++++---- .../electron-browser/localProcessExtensionHost.ts | 4 ++-- .../remote/common/abstractRemoteAgentService.ts | 4 ++-- .../themes/browser/workbenchThemeService.ts | 6 +++--- 41 files changed, 111 insertions(+), 76 deletions(-) diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 112d7231f1aeb..47de4f1de301e 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -738,6 +738,7 @@ class BranchNode implements ISplitView, IDisposable { } this._onDidChange.dispose(); + this._onDidScroll.dispose(); this._onDidSashReset.dispose(); this._onDidVisibilityChange.dispose(); @@ -933,6 +934,7 @@ class LeafNode implements ISplitView, IDisposable { } dispose(): void { + this._onDidSetLinkedNode.dispose(); this.disposables.dispose(); } } @@ -1832,6 +1834,7 @@ export class GridView implements IDisposable { } dispose(): void { + this._onDidChangeViewMaximized.dispose(); this.onDidSashResetRelay.dispose(); this.root.dispose(); this.element.remove(); diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 512430ae805de..978dbb0197e76 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -332,8 +332,8 @@ export class ListView implements IListView { private readonly disposables: DisposableStore = new DisposableStore(); - private readonly _onDidChangeContentHeight = new Emitter(); - private readonly _onDidChangeContentWidth = new Emitter(); + private readonly _onDidChangeContentHeight = this.disposables.add(new Emitter()); + private readonly _onDidChangeContentWidth = this.disposables.add(new Emitter()); readonly onDidChangeContentHeight: Event = Event.latch(this._onDidChangeContentHeight.event, undefined, this.disposables); readonly onDidChangeContentWidth: Event = Event.latch(this._onDidChangeContentWidth.event, undefined, this.disposables); get contentHeight(): number { return this.rangeMap.size; } diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 0ea558d7ff7b9..27c924f150090 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -126,7 +126,7 @@ class ColumnHeader extends Disposable implements IView { get maximumSize() { return this.column.maximumWidth ?? Number.POSITIVE_INFINITY; } get onDidChange() { return this.column.onDidChangeWidthConstraints ?? Event.None; } - private _onDidLayout = new Emitter<[number, number]>(); + private _onDidLayout = this._register(new Emitter<[number, number]>()); readonly onDidLayout = this._onDidLayout.event; constructor(readonly column: ITableColumn, private index: number) { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index ece9f2d016c0a..d8684d50200e4 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -832,7 +832,7 @@ class FindWidget extends Disposable { private readonly actionbar: ActionBar; private readonly toggles: TreeFindToggle[] = []; - readonly _onDidDisable = new Emitter(); + readonly _onDidDisable = this._register(new Emitter()); readonly onDidDisable = this._onDidDisable.event; readonly onDidChangeValue: Event; readonly onDidToggleChange: Event; @@ -1901,10 +1901,10 @@ class StickyScrollFocus extends Disposable { private elements: HTMLElement[] = []; private state: StickyScrollState | undefined; - private _onDidChangeHasFocus = new Emitter(); + private _onDidChangeHasFocus = this._register(new Emitter()); readonly onDidChangeHasFocus = this._onDidChangeHasFocus.event; - private _onContextMenu = new Emitter>(); + private _onContextMenu = this._register(new Emitter>()); readonly onContextMenu: Event> = this._onContextMenu.event; private _domHasFocus: boolean = false; diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 565340518ff9d..6c604269ac51a 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -1387,6 +1387,8 @@ export class AsyncDataTree implements IDisposable } dispose(): void { + this._onDidRender.dispose(); + this._onDidChangeNodeSlowState.dispose(); this.disposables.dispose(); this.tree.dispose(); } diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index 5908e831ba1c1..ccb2eca316f04 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -544,10 +544,10 @@ export class Protocol extends Disposable implements IMessagePassingProtocol { private _socketWriter: ProtocolWriter; private _socketReader: ProtocolReader; - private readonly _onMessage = new Emitter(); + private readonly _onMessage = this._register(new Emitter()); readonly onMessage: Event = this._onMessage.event; - private readonly _onDidDispose = new Emitter(); + private readonly _onDidDispose = this._register(new Emitter()); readonly onDidDispose: Event = this._onDidDispose.event; constructor(socket: ISocket) { diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index b22e9fd9e42b2..8e061681e5c51 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -780,6 +780,7 @@ export class ChannelClient implements IChannelClient, IDisposable { } dispose(this.activeRequests.values()); this.activeRequests.clear(); + this._onDidInitialize.dispose(); } } diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPickerModel.ts b/src/vs/editor/contrib/colorPicker/browser/colorPickerModel.ts index 06a0c278e273c..c3de9973b97dc 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPickerModel.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorPickerModel.ts @@ -5,9 +5,10 @@ import { Color } from '../../../../base/common/color.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { IColorPresentation } from '../../../common/languages.js'; -export class ColorPickerModel { +export class ColorPickerModel extends Disposable { readonly originalColor: Color; private _color: Color; @@ -41,16 +42,17 @@ export class ColorPickerModel { this._onDidChangePresentation.fire(this.presentation); } - private readonly _onColorFlushed = new Emitter(); + private readonly _onColorFlushed = this._register(new Emitter()); readonly onColorFlushed: Event = this._onColorFlushed.event; - private readonly _onDidChangeColor = new Emitter(); + private readonly _onDidChangeColor = this._register(new Emitter()); readonly onDidChangeColor: Event = this._onDidChangeColor.event; - private readonly _onDidChangePresentation = new Emitter(); + private readonly _onDidChangePresentation = this._register(new Emitter()); readonly onDidChangePresentation: Event = this._onDidChangePresentation.event; constructor(color: Color, availableColorPresentations: IColorPresentation[], private presentationIndex: number) { + super(); this.originalColor = color; this._color = color; this._colorPresentations = availableColorPresentations; diff --git a/src/vs/platform/debug/common/extensionHostDebugIpc.ts b/src/vs/platform/debug/common/extensionHostDebugIpc.ts index fb8b2a379220a..74e3b97956d2d 100644 --- a/src/vs/platform/debug/common/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/common/extensionHostDebugIpc.ts @@ -8,14 +8,14 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IAttachSessionEvent, ICloseSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult, IReloadSessionEvent, ITerminateSessionEvent } from './extensionHostDebug.js'; -export class ExtensionHostDebugBroadcastChannel implements IServerChannel { +export class ExtensionHostDebugBroadcastChannel extends Disposable implements IServerChannel { static readonly ChannelName = 'extensionhostdebugservice'; - private readonly _onCloseEmitter = new Emitter(); - private readonly _onReloadEmitter = new Emitter(); - private readonly _onTerminateEmitter = new Emitter(); - private readonly _onAttachEmitter = new Emitter(); + private readonly _onCloseEmitter = this._register(new Emitter()); + private readonly _onReloadEmitter = this._register(new Emitter()); + private readonly _onTerminateEmitter = this._register(new Emitter()); + private readonly _onAttachEmitter = this._register(new Emitter()); call(ctx: TContext, command: string, arg?: any): Promise { switch (command) { diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 2c04a5ca4f8f5..49eb6191d2f52 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -216,6 +216,7 @@ export class DisposableTunnel { dispose(): Promise { this._onDispose.fire(); + this._onDispose.dispose(); return this._dispose(); } } diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/common/updateIpc.ts index e6675d73f4009..9eaf8210757e2 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -41,7 +41,7 @@ export class UpdateChannelClient implements IUpdateService { declare readonly _serviceBrand: undefined; private readonly disposables = new DisposableStore(); - private readonly _onStateChange = new Emitter(); + private readonly _onStateChange = this.disposables.add(new Emitter()); readonly onStateChange: Event = this._onStateChange.event; private _state: State = State.Uninitialized; diff --git a/src/vs/server/node/extensionHostConnection.ts b/src/vs/server/node/extensionHostConnection.ts index 0daf9ee703182..e093c3f9b04eb 100644 --- a/src/vs/server/node/extensionHostConnection.ts +++ b/src/vs/server/node/extensionHostConnection.ts @@ -109,7 +109,7 @@ class ConnectionData { export class ExtensionHostConnection extends Disposable { - private _onClose = new Emitter(); + private _onClose = this._register(new Emitter()); readonly onClose: Event = this._onClose.event; private readonly _canSendSocket: boolean; diff --git a/src/vs/server/node/remoteExtensionManagement.ts b/src/vs/server/node/remoteExtensionManagement.ts index e2ac965cb16dd..df587cf746828 100644 --- a/src/vs/server/node/remoteExtensionManagement.ts +++ b/src/vs/server/node/remoteExtensionManagement.ts @@ -111,6 +111,7 @@ export class ManagementConnection { this.protocol.dispose(); socket.end(); this._onClose.fire(undefined); + this._onClose.dispose(); } public acceptReconnection(remoteAddress: string, socket: ISocket, initialDataChunk: VSBuffer): void { diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 5a7b3ad78cd79..193ad8729928b 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -216,10 +216,13 @@ export class MainThreadCommentThread implements languages.CommentThread { dispose() { this._isDisposed = true; this._onDidChangeCollapsibleState.dispose(); + this._onDidChangeInitialCollapsibleState.dispose(); this._onDidChangeComments.dispose(); this._onDidChangeInput.dispose(); this._onDidChangeLabel.dispose(); + this._onDidChangeCanReply.dispose(); this._onDidChangeState.dispose(); + this._onDidChangeApplicability.dispose(); } toJSON(): MarshalledCommentThread { diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 257e99854526a..03dfacf03b65b 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -205,6 +205,7 @@ export class MainThreadTextEditor { public dispose(): void { this._modelListeners.dispose(); + this._onPropertiesChanged.dispose(); this._codeEditor = null; this._codeEditorListeners.dispose(); } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 18430fbfffa87..42881dbe8b131 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -1290,10 +1290,10 @@ export class MainThreadDocumentRangeSemanticTokensProvider implements languages. class ExtensionBackedInlineCompletionsProvider extends Disposable implements languages.InlineCompletionsProvider { public readonly setModelId: ((modelId: string) => Promise) | undefined; - public readonly _onDidChangeEmitter = new Emitter(); + public readonly _onDidChangeEmitter = this._register(new Emitter()); public readonly onDidChangeInlineCompletions: Event | undefined; - public readonly _onDidChangeModelInfoEmitter = new Emitter(); + public readonly _onDidChangeModelInfoEmitter = this._register(new Emitter()); public readonly onDidChangeModelInfo: Event | undefined; constructor( diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index d86f50442d3a5..a15d68e0215f6 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -194,9 +194,11 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { const accountLabel = auth.accountLabel ?? localize('languageModelsAccountId', 'Language Models'); const disposables = new DisposableStore(); - this._authenticationService.registerAuthenticationProvider(authProviderId, new LanguageModelAccessAuthProvider(authProviderId, auth.providerLabel, accountLabel)); + const provider = new LanguageModelAccessAuthProvider(authProviderId, auth.providerLabel, accountLabel); + this._authenticationService.registerAuthenticationProvider(authProviderId, provider); disposables.add(toDisposable(() => { this._authenticationService.unregisterAuthenticationProvider(authProviderId); + provider.dispose(); })); disposables.add(this._authenticationAccessService.onDidChangeExtensionSessionAccess(async (e) => { const allowedExtensions = this._authenticationAccessService.readAllowedExtensions(authProviderId, accountLabel); @@ -282,4 +284,8 @@ class LanguageModelAccessAuthProvider implements IAuthenticationProvider { scopes, }; } + + dispose(): void { + this._onDidChangeSessions.dispose(); + } } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 217c513bc1ba5..b60febba917c8 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -596,6 +596,8 @@ class MainThreadSCMProvider implements ISCMProvider { } dispose(): void { + this._onDidChangeResourceGroups.dispose(); + this._onDidChangeResources.dispose(); this._artifactProvider.get()?.dispose(); this._stagedQuickDiff?.dispose(); this._quickDiff?.dispose(); diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 3ffef80d86295..5bd4fbf896920 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -407,6 +407,7 @@ export class DynamicAuthProvider implements vscode.AuthenticationProvider { this._logger = loggerService.createLogger(this.id, { name: `Auth: ${this.label}` }); this._disposable = new DisposableStore(); this._disposable.add(this._onDidChangeSessions); + this._disposable.add(this._onDidChangeClientId); const scopedEvent = Event.chain(onDidDynamicAuthProviderTokensChange.event, $ => $ .filter(e => e.authProviderId === this.id && e.clientId === _clientId) .map(e => e.tokens) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 4cc0bc3cf550d..bbf66df837fdc 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -1244,6 +1244,8 @@ class ExtHostChatAgent { disposed = true; that._followupProvider = undefined; that._onDidReceiveFeedback.dispose(); + that._onDidPerformAction.dispose(); + that._pauseStateEmitter.dispose(); that._proxy.$unregisterAgent(that._handle); }, } satisfies vscode.ChatParticipant; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 826b79501beb0..3f1378c31dd1f 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -585,6 +585,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo dispose() { this._isDiposed = true; this._acceptInputDisposables.dispose(); + this._onDidUpdateCommentThread.dispose(); this._localDisposables.forEach(disposable => disposable.dispose()); } } diff --git a/src/vs/workbench/api/common/extHostDocuments.ts b/src/vs/workbench/api/common/extHostDocuments.ts index ecbef1f099b61..517568ceb6daf 100644 --- a/src/vs/workbench/api/common/extHostDocuments.ts +++ b/src/vs/workbench/api/common/extHostDocuments.ts @@ -18,19 +18,19 @@ import { ISerializedModelContentChangedEvent } from '../../../editor/common/text export class ExtHostDocuments implements ExtHostDocumentsShape { - private readonly _onDidAddDocument = new Emitter(); - private readonly _onDidRemoveDocument = new Emitter(); - private readonly _onDidChangeDocument = new Emitter>(); - private readonly _onDidChangeDocumentWithReason = new Emitter(); - private readonly _onDidSaveDocument = new Emitter(); + private readonly _toDispose = new DisposableStore(); + + private readonly _onDidAddDocument = this._toDispose.add(new Emitter()); + private readonly _onDidRemoveDocument = this._toDispose.add(new Emitter()); + private readonly _onDidChangeDocument = this._toDispose.add(new Emitter>()); + private readonly _onDidChangeDocumentWithReason = this._toDispose.add(new Emitter()); + private readonly _onDidSaveDocument = this._toDispose.add(new Emitter()); readonly onDidAddDocument: Event = this._onDidAddDocument.event; readonly onDidRemoveDocument: Event = this._onDidRemoveDocument.event; readonly onDidChangeDocument: Event = this._onDidChangeDocument.event as Event; readonly onDidChangeDocumentWithReason: Event = this._onDidChangeDocumentWithReason.event; readonly onDidSaveDocument: Event = this._onDidSaveDocument.event; - - private readonly _toDispose = new DisposableStore(); private _proxy: MainThreadDocumentsShape; private _documentsAndEditors: ExtHostDocumentsAndEditors; private _documentLoader = new Map>(); diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index eecdf625b0088..26ba550555f25 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -576,7 +576,7 @@ class NotebookCellExecutionTask extends Disposable { private static HANDLE = 0; private _handle = NotebookCellExecutionTask.HANDLE++; - private _onDidChangeState = new Emitter(); + private _onDidChangeState = this._register(new Emitter()); readonly onDidChangeState = this._onDidChangeState.event; private _state = NotebookCellExecutionTaskState.Init; @@ -786,7 +786,7 @@ class NotebookExecutionTask extends Disposable { private static HANDLE = 0; private _handle = NotebookExecutionTask.HANDLE++; - private _onDidChangeState = new Emitter(); + private _onDidChangeState = this._register(new Emitter()); readonly onDidChangeState = this._onDidChangeState.event; private _state = NotebookExecutionTaskState.Init; diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index a7c37795384bb..0908fa588556b 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -539,6 +539,8 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG dispose(): void { this._disposed = true; this._onDidDispose.fire(); + this._onDidUpdateResourceStates.dispose(); + this._onDidDispose.dispose(); } } @@ -939,6 +941,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this._groups.forEach(group => group.dispose()); this.#proxy.$unregisterSourceControl(this.handle); + this._onDidChangeSelection.dispose(); this._onDidDispose.fire(); this._onDidDispose.dispose(); } diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index 716fc15ef27a1..93db4e49ab331 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -325,6 +325,7 @@ export class ExtHostTelemetryLogger { } else { this._sender = undefined; } + this._onDidChangeEnableStates.dispose(); } } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 7a44abb0eb1d3..225ed9c223111 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -1040,7 +1040,7 @@ class UnifiedEnvironmentVariableCollection extends Disposable { this._onDidChangeCollection.fire(); } - protected readonly _onDidChangeCollection: Emitter = new Emitter(); + protected readonly _onDidChangeCollection: Emitter = this._register(new Emitter()); get onDidChangeCollection(): Event { return this._onDidChangeCollection && this._onDidChangeCollection.event; } constructor( diff --git a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts index 2367f76ded64b..37a03148780d4 100644 --- a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts +++ b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts @@ -31,11 +31,11 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH private _activeShellIntegrations: Map = new Map(); - protected readonly _onDidChangeTerminalShellIntegration = new Emitter(); + protected readonly _onDidChangeTerminalShellIntegration = this._register(new Emitter()); readonly onDidChangeTerminalShellIntegration = this._onDidChangeTerminalShellIntegration.event; - protected readonly _onDidStartTerminalShellExecution = new Emitter(); + protected readonly _onDidStartTerminalShellExecution = this._register(new Emitter()); readonly onDidStartTerminalShellExecution = this._onDidStartTerminalShellExecution.event; - protected readonly _onDidEndTerminalShellExecution = new Emitter(); + protected readonly _onDidEndTerminalShellExecution = this._register(new Emitter()); readonly onDidEndTerminalShellExecution = this._onDidEndTerminalShellExecution.event; constructor( diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index 7655c14f8d7d1..74bd16808a440 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -17,13 +17,13 @@ import * as vscode from 'vscode'; export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { - private readonly _onDidChangeTextEditorSelection = new Emitter(); - private readonly _onDidChangeTextEditorOptions = new Emitter(); - private readonly _onDidChangeTextEditorVisibleRanges = new Emitter(); - private readonly _onDidChangeTextEditorViewColumn = new Emitter(); - private readonly _onDidChangeTextEditorDiffInformation = new Emitter(); - private readonly _onDidChangeActiveTextEditor = new Emitter(); - private readonly _onDidChangeVisibleTextEditors = new Emitter(); + private readonly _onDidChangeTextEditorSelection = this._register(new Emitter()); + private readonly _onDidChangeTextEditorOptions = this._register(new Emitter()); + private readonly _onDidChangeTextEditorVisibleRanges = this._register(new Emitter()); + private readonly _onDidChangeTextEditorViewColumn = this._register(new Emitter()); + private readonly _onDidChangeTextEditorDiffInformation = this._register(new Emitter()); + private readonly _onDidChangeActiveTextEditor = this._register(new Emitter()); + private readonly _onDidChangeVisibleTextEditors = this._register(new Emitter()); readonly onDidChangeTextEditorSelection: Event = this._onDidChangeTextEditorSelection.event; readonly onDidChangeTextEditorOptions: Event = this._onDidChangeTextEditorOptions.event; diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index de79ab6d9599a..a85ca72b95a8d 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -68,7 +68,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe private _forwardPortProvider: ((tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions, token?: vscode.CancellationToken) => Thenable | undefined) | undefined; private _showCandidatePort: (host: string, port: number, detail: string) => Thenable = () => { return Promise.resolve(true); }; private _extensionTunnels: Map> = new Map(); - private _onDidChangeTunnels: Emitter = new Emitter(); + private _onDidChangeTunnels: Emitter = this._register(new Emitter()); onDidChangeTunnels: vscode.Event = this._onDidChangeTunnels.event; private _providerHandleCounter: number = 0; diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index bbc655588bf4d..a75b18c653548 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -631,7 +631,7 @@ interface ICapabilitiesColumnTemplateData extends IModelTableColumnTemplateData readonly metadataRow: HTMLElement; } -class CapabilitiesColumnRenderer extends ModelsTableColumnRenderer { +class CapabilitiesColumnRenderer extends ModelsTableColumnRenderer implements IDisposable { static readonly TEMPLATE_ID = 'capabilities'; readonly templateId: string = CapabilitiesColumnRenderer.TEMPLATE_ID; @@ -639,6 +639,10 @@ class CapabilitiesColumnRenderer extends ModelsTableColumnRenderer(); readonly onDidClickCapability = this._onDidClickCapability.event; + dispose(): void { + this._onDidClickCapability.dispose(); + } + renderTemplate(container: HTMLElement): ICapabilitiesColumnTemplateData { const disposables = new DisposableStore(); const elementDisposables = new DisposableStore(); @@ -1007,6 +1011,7 @@ export class ChatModelsWidget extends Disposable { const actionsColumnRenderer = this.instantiationService.createInstance(ActionsColumnRenderer, this.viewModel); const providerColumnRenderer = this.instantiationService.createInstance(ProviderColumnRenderer); + this.tableDisposables.add(capabilitiesColumnRenderer); this.tableDisposables.add(capabilitiesColumnRenderer.onDidClickCapability(capability => { const currentQuery = this.searchWidget.getValue(); const query = `@capability:${capability}`; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts index 7dcf175a3f470..4fd9d3e5337a0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts @@ -9,7 +9,7 @@ import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button. import { IListRenderer, IListVirtualDelegate } from '../../../../../../base/browser/ui/list/list.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, IObservable } from '../../../../../../base/common/observable.js'; import { isEqual } from '../../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; @@ -112,14 +112,14 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl private renderViewAllFileChangesButton(container: HTMLElement): IDisposable { const button = container.appendChild($('.chat-view-changes-icon')); - this.hoverService.setupDelayedHover(button, () => ({ + const hoverDisposable = this.hoverService.setupDelayedHover(button, () => ({ content: localize2('chat.viewFileChangesSummary', 'View All File Changes') })); button.classList.add(...ThemeIcon.asClassNameArray(Codicon.diffMultiple)); button.setAttribute('role', 'button'); button.tabIndex = 0; - return dom.addDisposableListener(button, 'click', (e) => { + return combinedDisposable(hoverDisposable, dom.addDisposableListener(button, 'click', (e) => { const resources: { originalUri: URI; modifiedUri?: URI }[] = this.fileChangesDiffsObservable.get().map(diff => ({ originalUri: diff.originalURI, modifiedUri: diff.modifiedURI @@ -141,7 +141,7 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl ); this.editorGroupsService.activeGroup.openEditor(input); dom.EventHelper.stop(e, true); - }); + })); } private renderFilesList(container: HTMLElement): IDisposable { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts index bcaa53fee11d0..d78995dd4534d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/codeBlockPart.ts @@ -221,7 +221,7 @@ export class CodeBlockPart extends Disposable { }); const toolbarElement = dom.append(this.element, $('.interactive-result-code-block-toolbar')); - const editorScopedService = this.editor.contextKeyService.createScoped(toolbarElement); + const editorScopedService = this._register(this.editor.contextKeyService.createScoped(toolbarElement)); const editorScopedInstantiationService = this._register(scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService]))); this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarElement, menuId, { menuOptions: { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 3a54c6236d8df..00bc6005901b1 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -152,7 +152,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget @IDialogService private readonly dialogService: IDialogService ) { super(editor, { keepEditorSelection: true, isAccessible: true, showArrow: !!_commentThread.range }); - this._contextKeyService = contextKeyService.createScoped(this.domNode); + this._contextKeyService = this._globalToDispose.add(contextKeyService.createScoped(this.domNode)); this._scopedInstantiationService = this._globalToDispose.add(instantiationService.createChild(new ServiceCollection( [IContextKeyService, this._contextKeyService] diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index ce109ad4f2941..263cc326515ff 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -48,12 +48,12 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes return this.existingSessionId !== undefined; } - private _didSignIn = new Emitter(); + private _didSignIn = this._register(new Emitter()); get onDidSignIn() { return this._didSignIn.event; } - private _didSignOut = new Emitter(); + private _didSignOut = this._register(new Emitter()); get onDidSignOut() { return this._didSignOut.event; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 669dd15c1c9d4..dd3439f21f933 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1502,7 +1502,7 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { private _cancelAction: IAction; - private _onDidChange = new Emitter(); + private _onDidChange = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; private readonly _disposables = this._register(new MutableDisposable()); @@ -1933,7 +1933,7 @@ class SCMInputWidget { this.editorContainer = append(this.element, $('.scm-editor-container')); this.toolbarContainer = append(this.element, $('.scm-editor-toolbar')); - this.contextKeyService = contextKeyService.createScoped(this.element); + this.contextKeyService = this.disposables.add(contextKeyService.createScoped(this.element)); this.repositoryIdContextKey = this.contextKeyService.createKey('scmRepository', undefined); this.validationMessageContextKey = ContextKeys.SCMInputHasValidationMessage.bindTo(this.contextKeyService); @@ -2190,7 +2190,7 @@ class SCMInputWidget { export class SCMViewPane extends ViewPane { - private _onDidLayout: Emitter; + private readonly _onDidLayout: Emitter; private layoutCache: ISCMLayout; private treeScrollTop: number | undefined; @@ -2222,7 +2222,7 @@ export class SCMViewPane extends ViewPane { this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); } - private readonly _onDidChangeViewMode = new Emitter(); + private readonly _onDidChangeViewMode = this._register(new Emitter()); readonly onDidChangeViewMode = this._onDidChangeViewMode.event; private _viewSortKey: ViewSortKey; @@ -2243,7 +2243,7 @@ export class SCMViewPane extends ViewPane { } } - private readonly _onDidChangeViewSortKey = new Emitter(); + private readonly _onDidChangeViewSortKey = this._register(new Emitter()); readonly onDidChangeViewSortKey = this._onDidChangeViewSortKey.event; private readonly items = new DisposableMap(); @@ -2300,7 +2300,7 @@ export class SCMViewPane extends ViewPane { this.scmProviderRootUriContextKey = ContextKeys.SCMProviderRootUri.bindTo(contextKeyService); this.scmProviderHasRootUriContextKey = ContextKeys.SCMProviderHasRootUri.bindTo(contextKeyService); - this._onDidLayout = new Emitter(); + this._onDidLayout = this._register(new Emitter()); this.layoutCache = { height: undefined, width: undefined, onDidChange: this._onDidLayout.event }; this.storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, this.disposables)(e => { diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLink.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLink.ts index 228d047f4b254..aef7d87a3cd19 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLink.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLink.ts @@ -22,7 +22,7 @@ export class TerminalLink extends Disposable implements ILink { private readonly _tooltipScheduler: MutableDisposable = this._register(new MutableDisposable()); private readonly _hoverListeners = this._register(new MutableDisposable()); - private readonly _onInvalidated = new Emitter(); + private readonly _onInvalidated = this._register(new Emitter()); get onInvalidated(): Event { return this._onInvalidated.event; } get type(): TerminalLinkType { return this._type; } diff --git a/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts index 66b343c557439..b7d264ee0c84b 100644 --- a/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts @@ -715,7 +715,7 @@ export class PredictionStats extends Disposable { } } -export class PredictionTimeline { +export class PredictionTimeline extends Disposable { /** * Expected queue of events. Only predictions for the lowest are * written into the terminal. @@ -760,11 +760,11 @@ export class PredictionTimeline { */ private _lookBehind?: IPrediction; - private readonly _addedEmitter = new Emitter(); + private readonly _addedEmitter = this._register(new Emitter()); readonly onPredictionAdded = this._addedEmitter.event; - private readonly _failedEmitter = new Emitter(); + private readonly _failedEmitter = this._register(new Emitter()); readonly onPredictionFailed = this._failedEmitter.event; - private readonly _succeededEmitter = new Emitter(); + private readonly _succeededEmitter = this._register(new Emitter()); readonly onPredictionSucceeded = this._succeededEmitter.event; private get _currentGenerationPredictions() { @@ -779,7 +779,7 @@ export class PredictionTimeline { return this._expected.length; } - constructor(readonly terminal: Terminal, private readonly _style: TypeAheadStyle) { } + constructor(readonly terminal: Terminal, private readonly _style: TypeAheadStyle) { super(); } setShowPredictions(show: boolean) { if (show === this._showPredictions) { @@ -1321,7 +1321,7 @@ export class TypeAheadAddon extends Disposable implements ITerminalAddon { activate(terminal: Terminal): void { const style = this._typeaheadStyle = this._register(new TypeAheadStyle(this._configurationService.getValue(TERMINAL_CONFIG_SECTION).localEchoStyle, terminal)); - const timeline = this._timeline = new PredictionTimeline(terminal, this._typeaheadStyle); + const timeline = this._timeline = this._register(new PredictionTimeline(terminal, this._typeaheadStyle)); const stats = this.stats = this._register(new PredictionStats(this._timeline)); timeline.setShowPredictions(this._typeaheadThreshold === 0); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 2ae37f9fd8f51..ac89ae8e93179 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -121,13 +121,13 @@ const NEW_WALKTHROUGH_TIME = 7 * DAYS; export class WalkthroughsService extends Disposable implements IWalkthroughsService { declare readonly _serviceBrand: undefined; - private readonly _onDidAddWalkthrough = new Emitter(); + private readonly _onDidAddWalkthrough = this._register(new Emitter()); readonly onDidAddWalkthrough: Event = this._onDidAddWalkthrough.event; - private readonly _onDidRemoveWalkthrough = new Emitter(); + private readonly _onDidRemoveWalkthrough = this._register(new Emitter()); readonly onDidRemoveWalkthrough: Event = this._onDidRemoveWalkthrough.event; - private readonly _onDidChangeWalkthrough = new Emitter(); + private readonly _onDidChangeWalkthrough = this._register(new Emitter()); readonly onDidChangeWalkthrough: Event = this._onDidChangeWalkthrough.event; - private readonly _onDidProgressStep = new Emitter(); + private readonly _onDidProgressStep = this._register(new Emitter()); readonly onDidProgressStep: Event = this._onDidProgressStep.event; private memento: Memento>; diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 6676c97550bad..d5bfbd3b5624d 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -249,8 +249,8 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte // Catch all output coming from the extension host process type Output = { data: string; format: string[] }; - const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout); - const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr); + const onStdout = this._register(this._handleProcessOutputStream(this._extensionHostProcess.onStdout)); + const onStderr = this._register(this._handleProcessOutputStream(this._extensionHostProcess.onStderr)); const onOutput = Event.any( Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })), Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] })) diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 1d2a47b2bcb29..c9071cc3c19e0 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -246,9 +246,9 @@ class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection this._initialConnectionMs = Date.now() - start; } - connection.protocol.onDidDispose(() => { + this._register(connection.protocol.onDidDispose(() => { connection.dispose(); - }); + })); this.end = () => { connection.protocol.sendDisconnect(); return connection.protocol.drain(); diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 8884c7958f87f..1a4648762e31f 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -118,20 +118,20 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme this.colorThemeRegistry = this._register(new ThemeRegistry(colorThemesExtPoint, ColorThemeData.fromExtensionTheme)); this.colorThemeWatcher = this._register(new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this))); - this.onColorThemeChange = new Emitter({ leakWarningThreshold: 400 }); + this.onColorThemeChange = this._register(new Emitter({ leakWarningThreshold: 400 })); this.currentColorTheme = ColorThemeData.createUnloadedTheme(''); this.colorThemeSequencer = new Sequencer(); this.fileIconThemeWatcher = this._register(new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentFileIconTheme.bind(this))); this.fileIconThemeRegistry = this._register(new ThemeRegistry(fileIconThemesExtPoint, FileIconThemeData.fromExtensionTheme, true, FileIconThemeData.noIconTheme)); this.fileIconThemeLoader = new FileIconThemeLoader(extensionResourceLoaderService, languageService); - this.onFileIconThemeChange = new Emitter({ leakWarningThreshold: 400 }); + this.onFileIconThemeChange = this._register(new Emitter({ leakWarningThreshold: 400 })); this.currentFileIconTheme = FileIconThemeData.createUnloadedTheme(''); this.fileIconThemeSequencer = new Sequencer(); this.productIconThemeWatcher = this._register(new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentProductIconTheme.bind(this))); this.productIconThemeRegistry = this._register(new ThemeRegistry(productIconThemesExtPoint, ProductIconThemeData.fromExtensionTheme, true, ProductIconThemeData.defaultTheme)); - this.onProductIconThemeChange = new Emitter(); + this.onProductIconThemeChange = this._register(new Emitter()); this.currentProductIconTheme = ProductIconThemeData.createUnloadedTheme(''); this.productIconThemeSequencer = new Sequencer(); From 27b86b5ce798891919aba1e727acb02d3634413e Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Mon, 16 Feb 2026 08:29:10 +0100 Subject: [PATCH 07/12] Update dependency list --- build/linux/rpm/dep-lists.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 783923f34d94f..0424c8d37a62d 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -52,6 +52,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.3.3)(64bit)', 'libc.so.6(GLIBC_2.3.4)(64bit)', 'libc.so.6(GLIBC_2.4)(64bit)', + 'libc.so.6(GLIBC_2.5)(64bit)', 'libc.so.6(GLIBC_2.6)(64bit)', 'libc.so.6(GLIBC_2.7)(64bit)', 'libc.so.6(GLIBC_2.8)(64bit)', @@ -144,6 +145,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.27)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', + 'libc.so.6(GLIBC_2.5)', 'libc.so.6(GLIBC_2.6)', 'libc.so.6(GLIBC_2.7)', 'libc.so.6(GLIBC_2.8)', From 1a6199f04b7eab5d7221e84929810a441c242b22 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Mon, 16 Feb 2026 00:22:57 -0800 Subject: [PATCH 08/12] Add Toggle Metered Connection command and update setting (#295522) --- .../meteredConnection.config.contribution.ts | 14 +++-- .../common/meteredConnection.ts | 21 ++++--- .../browser/meteredConnection.contribution.ts | 56 +++++++++++++++++++ .../browser/meteredConnectionStatus.ts | 11 ++-- ...scode.proposed.envIsConnectionMetered.d.ts | 2 +- 5 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts b/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts index eeff32a25eac5..1259f403a12ec 100644 --- a/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts +++ b/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts @@ -14,11 +14,17 @@ configurationRegistry.registerConfiguration({ title: localize('networkConfigurationTitle', "Network"), type: 'object', properties: { - 'network.respectMeteredConnections': { - type: 'boolean', - default: true, + 'network.meteredConnection': { + type: 'string', + enum: ['auto', 'on', 'off'], + enumDescriptions: [ + localize('meteredConnection.auto', "Automatically detect metered connections using the operating system's network status."), + localize('meteredConnection.on', "Always treat the network connection as metered. Automatic updates and downloads will be postponed."), + localize('meteredConnection.off', "Never treat the network connection as metered.") + ], + default: 'auto', scope: ConfigurationScope.APPLICATION, - description: localize('respectMeteredConnections', "When enabled, automatic updates and downloads will be postponed when on a metered network connection (such as mobile data or tethering)."), + description: localize('meteredConnection', "Controls whether the current network connection should be treated as metered. When metered, automatic updates, extension downloads, and other background network activity will be postponed to reduce data usage."), tags: ['usesOnlineServices'] } } diff --git a/src/vs/platform/meteredConnection/common/meteredConnection.ts b/src/vs/platform/meteredConnection/common/meteredConnection.ts index 321aec6c5403c..2eb6796aae722 100644 --- a/src/vs/platform/meteredConnection/common/meteredConnection.ts +++ b/src/vs/platform/meteredConnection/common/meteredConnection.ts @@ -18,7 +18,8 @@ export interface IMeteredConnectionService { /** * Whether the current network connection is metered. - * Always returns `false` if the `network.respectMeteredConnections` setting is disabled. + * Always returns `false` if the `network.meteredConnection` setting is `off`. + * Always returns `true` if the `network.meteredConnection` setting is `on`. */ readonly isConnectionMetered: boolean; @@ -28,7 +29,9 @@ export interface IMeteredConnectionService { readonly onDidChangeIsConnectionMetered: Event; } -export const METERED_CONNECTION_SETTING_KEY = 'network.respectMeteredConnections'; +export const METERED_CONNECTION_SETTING_KEY = 'network.meteredConnection'; + +export type MeteredConnectionSettingValue = 'on' | 'off' | 'auto'; /** * Network Information API @@ -77,20 +80,20 @@ export abstract class AbstractMeteredConnectionService extends Disposable implem private _isConnectionMetered: boolean; private _isBrowserConnectionMetered: boolean; - private _respectMeteredConnections: boolean; + private _meteredConnectionSetting: MeteredConnectionSettingValue; constructor(configurationService: IConfigurationService, isBrowserConnectionMetered: boolean) { super(); this._isBrowserConnectionMetered = isBrowserConnectionMetered; - this._respectMeteredConnections = configurationService.getValue(METERED_CONNECTION_SETTING_KEY); - this._isConnectionMetered = this._respectMeteredConnections && this._isBrowserConnectionMetered; + this._meteredConnectionSetting = configurationService.getValue(METERED_CONNECTION_SETTING_KEY); + this._isConnectionMetered = this._meteredConnectionSetting === 'on' || (this._meteredConnectionSetting !== 'off' && this._isBrowserConnectionMetered); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(METERED_CONNECTION_SETTING_KEY)) { - const value = configurationService.getValue(METERED_CONNECTION_SETTING_KEY); - if (value !== this._respectMeteredConnections) { - this._respectMeteredConnections = value; + const value = configurationService.getValue(METERED_CONNECTION_SETTING_KEY); + if (value !== this._meteredConnectionSetting) { + this._meteredConnectionSetting = value; this.onUpdated(); } } @@ -117,7 +120,7 @@ export abstract class AbstractMeteredConnectionService extends Disposable implem } protected onUpdated() { - const value = this._respectMeteredConnections && this._isBrowserConnectionMetered; + const value = this._meteredConnectionSetting === 'on' || (this._meteredConnectionSetting !== 'off' && this._isBrowserConnectionMetered); if (value !== this._isConnectionMetered) { this._isConnectionMetered = value; this.onChangeIsConnectionMetered(); diff --git a/src/vs/workbench/contrib/meteredConnection/browser/meteredConnection.contribution.ts b/src/vs/workbench/contrib/meteredConnection/browser/meteredConnection.contribution.ts index 66e27614968c6..9e8acd82bc36b 100644 --- a/src/vs/workbench/contrib/meteredConnection/browser/meteredConnection.contribution.ts +++ b/src/vs/workbench/contrib/meteredConnection/browser/meteredConnection.contribution.ts @@ -3,9 +3,65 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize, localize2 } from '../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { METERED_CONNECTION_SETTING_KEY, MeteredConnectionSettingValue } from '../../../../platform/meteredConnection/common/meteredConnection.js'; +import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { MeteredConnectionStatusContribution } from './meteredConnectionStatus.js'; import '../../../../platform/meteredConnection/common/meteredConnection.config.contribution.js'; registerWorkbenchContribution2(MeteredConnectionStatusContribution.ID, MeteredConnectionStatusContribution, WorkbenchPhase.AfterRestored); + +registerAction2(class ConfigureMeteredConnectionAction extends Action2 { + + static readonly ID = 'workbench.action.configureMeteredConnection'; + + constructor() { + super({ + id: ConfigureMeteredConnectionAction.ID, + title: localize2('configureMeteredConnection', 'Configure Metered Connection'), + f1: true + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const quickInputService = accessor.get(IQuickInputService); + const configurationService = accessor.get(IConfigurationService); + + const currentValue = configurationService.getValue(METERED_CONNECTION_SETTING_KEY); + + const picks: (IQuickPickItem & { value: MeteredConnectionSettingValue })[] = [ + { + value: 'auto', + label: localize('meteredConnection.auto', "Auto"), + description: localize('meteredConnection.auto.description', "Detect metered connections automatically"), + picked: currentValue === 'auto' + }, + { + value: 'on', + label: localize('meteredConnection.on', "On"), + description: localize('meteredConnection.on.description', "Always treat the connection as metered"), + picked: currentValue === 'on' + }, + { + value: 'off', + label: localize('meteredConnection.off', "Off"), + description: localize('meteredConnection.off.description', "Never treat the connection as metered"), + picked: currentValue === 'off' + } + ]; + + const pick = await quickInputService.pick(picks, { + placeHolder: localize('meteredConnection.placeholder', "Select Metered Connection Mode"), + activeItem: picks.find(p => p.picked) + }); + + if (pick) { + await configurationService.updateValue(METERED_CONNECTION_SETTING_KEY, pick.value); + } + } +}); diff --git a/src/vs/workbench/contrib/meteredConnection/browser/meteredConnectionStatus.ts b/src/vs/workbench/contrib/meteredConnection/browser/meteredConnectionStatus.ts index 619ecfcf04305..677ddbc62521b 100644 --- a/src/vs/workbench/contrib/meteredConnection/browser/meteredConnectionStatus.ts +++ b/src/vs/workbench/contrib/meteredConnection/browser/meteredConnectionStatus.ts @@ -5,7 +5,7 @@ import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; -import { IMeteredConnectionService, METERED_CONNECTION_SETTING_KEY } from '../../../../platform/meteredConnection/common/meteredConnection.js'; +import { IMeteredConnectionService } from '../../../../platform/meteredConnection/common/meteredConnection.js'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; @@ -47,12 +47,11 @@ export class MeteredConnectionStatusContribution extends Disposable implements I return { name: localize('status.meteredConnection', "Metered Connection"), text: '$(radio-tower)', - ariaLabel: localize('status.meteredConnection.ariaLabel', "Metered Connection Detected"), - tooltip: localize('status.meteredConnection.tooltip', "Metered connection detected. Some automatic features like extension updates, Settings Sync, and automatic Git operations are paused to reduce data usage."), + ariaLabel: localize('status.meteredConnection.ariaLabel', "Metered Connection Enabled"), + tooltip: localize('status.meteredConnection.tooltip', "Metered connection enabled. Some automatic features like extension updates, Settings Sync, and automatic Git operations are paused to reduce data usage."), command: { - id: 'workbench.action.openSettings', - title: localize('status.meteredConnection.configure', "Configure"), - arguments: [`@id:${METERED_CONNECTION_SETTING_KEY}`] + id: 'workbench.action.configureMeteredConnection', + title: localize('status.meteredConnection.configure', "Configure") }, showInAllWindows: true }; diff --git a/src/vscode-dts/vscode.proposed.envIsConnectionMetered.d.ts b/src/vscode-dts/vscode.proposed.envIsConnectionMetered.d.ts index fc89a57af7ace..62051fe8b0465 100644 --- a/src/vscode-dts/vscode.proposed.envIsConnectionMetered.d.ts +++ b/src/vscode-dts/vscode.proposed.envIsConnectionMetered.d.ts @@ -8,7 +8,7 @@ declare module 'vscode' { export namespace env { /** * Whether the current network connection is metered (such as mobile data or tethering). - * Always returns `false` if the `update.respectMeteredConnections` setting is disabled. + * Always returns `false` if the `network.meteredConnection` setting is set to `off`. */ export const isMeteredConnection: boolean; From 253145f150d3e584e48fc401443e8fa8d9b1825c Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Mon, 16 Feb 2026 00:23:36 -0800 Subject: [PATCH 09/12] Add a setting to control double-click to select block behavior (#295524) --- src/vs/editor/browser/view/viewController.ts | 9 ++++++--- src/vs/editor/common/config/editorOptions.ts | 12 +++++++++++- src/vs/editor/common/standalone/standaloneEnums.ts | 3 ++- src/vs/monaco.d.ts | 9 ++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 2066a317d0c15..5acc299c8ef3a 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -264,9 +264,12 @@ export class ViewController { if (data.inSelectionMode) { this._wordSelectDrag(data.position, data.revealType); } else { - const model = this.viewModel.model; - const modelPos = this._convertViewToModelPosition(data.position); - const selection = ViewController._trySelectBracketContent(model, modelPos) || ViewController._trySelectStringContent(model, modelPos); + let selection: Selection | undefined; + if (options.get(EditorOption.doubleClickSelectsBlock)) { + const model = this.viewModel.model; + const modelPos = this._convertViewToModelPosition(data.position); + selection = ViewController._trySelectBracketContent(model, modelPos) || ViewController._trySelectStringContent(model, modelPos); + } if (selection) { this._select(selection); } else { diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index acc23078daea8..34cdd09c350c4 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -566,6 +566,11 @@ export interface IEditorOptions { * Defaults to false. */ formatOnPaste?: boolean; + /** + * Controls whether double-clicking next to a bracket or quote selects the content inside. + * Defaults to true. + */ + doubleClickSelectsBlock?: boolean; /** * Controls if the editor should allow to move selections via drag and drop. * Defaults to false. @@ -5915,7 +5920,8 @@ export const enum EditorOption { inlineCompletionsAccessibilityVerbose, effectiveEditContext, scrollOnMiddleClick, - effectiveAllowVariableFonts + effectiveAllowVariableFonts, + doubleClickSelectsBlock } export const EditorOptions = { @@ -6208,6 +6214,10 @@ export const EditorOptions = { domReadOnly: register(new EditorBooleanOption( EditorOption.domReadOnly, 'domReadOnly', false, )), + doubleClickSelectsBlock: register(new EditorBooleanOption( + EditorOption.doubleClickSelectsBlock, 'doubleClickSelectsBlock', true, + { description: nls.localize('doubleClickSelectsBlock', "Controls whether double-clicking next to a bracket or quote selects the content inside.") } + )), dragAndDrop: register(new EditorBooleanOption( EditorOption.dragAndDrop, 'dragAndDrop', true, { description: nls.localize('dragAndDrop', "Controls whether the editor should allow moving selections via drag and drop.") } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 1caf3ced18fa7..cb0db8b268d0e 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -346,7 +346,8 @@ export enum EditorOption { inlineCompletionsAccessibilityVerbose = 169, effectiveEditContext = 170, scrollOnMiddleClick = 171, - effectiveAllowVariableFonts = 172 + effectiveAllowVariableFonts = 172, + doubleClickSelectsBlock = 173 } /** diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 80497d1a193b3..ea558fdbd7c01 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3721,6 +3721,11 @@ declare namespace monaco.editor { * Defaults to false. */ formatOnPaste?: boolean; + /** + * Controls whether double-clicking next to a bracket or quote selects the content inside. + * Defaults to true. + */ + doubleClickSelectsBlock?: boolean; /** * Controls if the editor should allow to move selections via drag and drop. * Defaults to false. @@ -5242,7 +5247,8 @@ declare namespace monaco.editor { inlineCompletionsAccessibilityVerbose = 169, effectiveEditContext = 170, scrollOnMiddleClick = 171, - effectiveAllowVariableFonts = 172 + effectiveAllowVariableFonts = 172, + doubleClickSelectsBlock = 173 } export const EditorOptions: { @@ -5291,6 +5297,7 @@ declare namespace monaco.editor { disableLayerHinting: IEditorOption; disableMonospaceOptimizations: IEditorOption; domReadOnly: IEditorOption; + doubleClickSelectsBlock: IEditorOption; dragAndDrop: IEditorOption; emptySelectionClipboard: IEditorOption; dropIntoEditor: IEditorOption>>; From d2a11ae372c88fdd5fbf5ac9bcfbb4326b7e9fa5 Mon Sep 17 00:00:00 2001 From: Nick Trogh Date: Mon, 16 Feb 2026 09:56:16 +0100 Subject: [PATCH 10/12] Improve chat tip messages --- src/vs/workbench/contrib/chat/browser/chatTipService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index deed7959ada39..38b5d9fa91b03 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -182,7 +182,7 @@ const TIP_CATALOG: ITipDefinition[] = [ }, { id: 'tip.customInstructions', - message: localize('tip.customInstructions', "Tip: [Generate workspace instructions](command:workbench.action.chat.generateInstructions) to give the agent relevant project-specific context when starting a task."), + message: localize('tip.customInstructions', "Tip: [Generate workspace instructions](command:workbench.action.chat.generateInstructions) apply coding conventions across all agent sessions."), enabledCommands: ['workbench.action.chat.generateInstructions'], excludeWhenPromptFilesExist: { promptType: PromptsType.instructions, agentFileType: AgentFileType.copilotInstructionsMd, excludeUntilChecked: true }, }, @@ -196,7 +196,7 @@ const TIP_CATALOG: ITipDefinition[] = [ }, { id: 'tip.skill', - message: localize('tip.skill', "Tip: [Create a skill](command:workbench.command.new.skill) to apply domain-specific workflows and instructions, only when needed."), + message: localize('tip.skill', "Tip: [Create a skill](command:workbench.command.new.skill) to teach the agent specialized workflows, loaded only when relevant."), when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), enabledCommands: ['workbench.command.new.skill'], excludeWhenCommandsExecuted: ['workbench.command.new.skill'], @@ -204,7 +204,7 @@ const TIP_CATALOG: ITipDefinition[] = [ }, { id: 'tip.messageQueueing', - message: localize('tip.messageQueueing', "Tip: Send follow-up and steering messages while the agent is working. They'll be queued and processed in order."), + message: localize('tip.messageQueueing', "Tip: Steer the agent mid-task by sending follow-up messages. They queue and apply in order."), when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), excludeWhenCommandsExecuted: ['workbench.action.chat.queueMessage', 'workbench.action.chat.steerWithMessage'], }, From 4f5d8f3946364ffbd52b7ffa971ffc1b3896be00 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 16 Feb 2026 10:03:47 +0100 Subject: [PATCH 11/12] Remove code-no-native-private ESLint rule (#295535) --- .../code-no-native-private.ts | 35 ------------------- eslint.config.js | 1 - extensions/git/src/api/api1.ts | 2 -- .../api/common/extHostChatSessions.ts | 1 - .../workbench/api/common/extHostCommands.ts | 2 -- .../api/common/extHostDiagnostics.ts | 2 -- .../api/common/extHostExtensionService.ts | 2 -- src/vs/workbench/api/common/extHostSCM.ts | 2 -- src/vs/workbench/api/common/extHostSecrets.ts | 2 -- .../workbench/api/common/extHostStatusBar.ts | 2 -- src/vs/workbench/api/common/extHostTask.ts | 2 -- src/vs/workbench/api/common/extHostTesting.ts | 2 -- src/vs/workbench/api/common/extHostTypes.ts | 2 -- .../api/common/extHostTypes/markdownString.ts | 2 -- .../api/common/extHostTypes/notebooks.ts | 2 -- src/vs/workbench/api/common/extHostWebview.ts | 2 -- .../api/common/extHostWebviewPanels.ts | 2 -- .../api/common/extHostWebviewView.ts | 2 -- 18 files changed, 67 deletions(-) delete mode 100644 .eslint-plugin-local/code-no-native-private.ts diff --git a/.eslint-plugin-local/code-no-native-private.ts b/.eslint-plugin-local/code-no-native-private.ts deleted file mode 100644 index 5d945ec34f7a5..0000000000000 --- a/.eslint-plugin-local/code-no-native-private.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as eslint from 'eslint'; -import type * as ESTree from 'estree'; - -export default new class ApiProviderNaming implements eslint.Rule.RuleModule { - - readonly meta: eslint.Rule.RuleMetaData = { - messages: { - slow: 'Native private fields are much slower and should only be used when needed. Ignore this warning if you know what you are doing, use compile-time private otherwise. See https://github.com/microsoft/vscode/issues/185991#issuecomment-1614468158 for details', - }, - schema: false, - }; - - create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - - return { - ['PropertyDefinition PrivateIdentifier']: (node: ESTree.Node) => { - context.report({ - node, - messageId: 'slow' - }); - }, - ['MethodDefinition PrivateIdentifier']: (node: ESTree.Node) => { - context.report({ - node, - messageId: 'slow' - }); - } - }; - } -}; diff --git a/eslint.config.js b/eslint.config.js index a0bcfc0556037..c2b3e29d446ac 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -77,7 +77,6 @@ export default tseslint.config( 'no-var': 'warn', 'semi': 'warn', 'local/code-translation-remind': 'warn', - 'local/code-no-native-private': 'warn', 'local/code-no-declare-const-enum': 'warn', 'local/code-parameter-properties-must-have-explicit-accessibility': 'warn', 'local/code-no-nls-in-standalone-editor': 'warn', diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 80aebb0616af1..0791401665ec0 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions, CommitShortStat, DiffChange, Worktree, RepositoryKind, RepositoryAccessDetails } from './git'; diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index f34f01a621a27..e62fe6439249e 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ import type * as vscode from 'vscode'; import { coalesce } from '../../../base/common/arrays.js'; diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 161e19baa3a5e..15c769373b8e5 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { validateConstraint } from '../../../base/common/types.js'; import { ICommandMetadata } from '../../../platform/commands/common/commands.js'; import * as extHostTypes from './extHostTypes.js'; diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index d3e84c8d05f0d..565959c5ddf54 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { localize } from '../../../nls.js'; import { IMarkerData, MarkerSeverity } from '../../../platform/markers/common/markers.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 215f56cc66fe7..a0178f266d4a8 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import * as nls from '../../../nls.js'; import * as path from '../../../base/common/path.js'; import * as performance from '../../../base/common/performance.js'; diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 0908fa588556b..bf479e9ebb196 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { URI, UriComponents } from '../../../base/common/uri.js'; import { Event, Emitter } from '../../../base/common/event.js'; import { debounce } from '../../../base/common/decorators.js'; diff --git a/src/vs/workbench/api/common/extHostSecrets.ts b/src/vs/workbench/api/common/extHostSecrets.ts index eeada39be07a3..1015639d9db19 100644 --- a/src/vs/workbench/api/common/extHostSecrets.ts +++ b/src/vs/workbench/api/common/extHostSecrets.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import type * as vscode from 'vscode'; import { ExtHostSecretState } from './extHostSecretState.js'; diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index 4cb635b3d5381..3c33dd1babeeb 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { StatusBarAlignment as ExtHostStatusBarAlignment, Disposable, ThemeColor, asStatusBarItemIdentifier } from './extHostTypes.js'; import type * as vscode from 'vscode'; import { MainContext, MainThreadStatusBarShape, IMainContext, ICommandDto, ExtHostStatusBarShape, StatusBarItemDto } from './extHost.protocol.js'; diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 6ac41d12fbd7e..5bf476c980a67 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { URI, UriComponents } from '../../../base/common/uri.js'; import { asPromise } from '../../../base/common/async.js'; import { Event, Emitter } from '../../../base/common/event.js'; diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 423b6d3b33a28..e1fa001aeee9c 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import type * as vscode from 'vscode'; import { RunOnceScheduler } from '../../../base/common/async.js'; import { VSBuffer } from '../../../base/common/buffer.js'; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index fb8fc1a348d45..0b762be0bdc58 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import type * as vscode from 'vscode'; import { asArray } from '../../../base/common/arrays.js'; import { VSBuffer } from '../../../base/common/buffer.js'; diff --git a/src/vs/workbench/api/common/extHostTypes/markdownString.ts b/src/vs/workbench/api/common/extHostTypes/markdownString.ts index c7840b9f74ab3..98b43ba3e82ec 100644 --- a/src/vs/workbench/api/common/extHostTypes/markdownString.ts +++ b/src/vs/workbench/api/common/extHostTypes/markdownString.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import type * as vscode from 'vscode'; import { MarkdownString as BaseMarkdownString, MarkdownStringTrustedOptions } from '../../../../base/common/htmlContent.js'; import { es5ClassCompat } from './es5ClassCompat.js'; diff --git a/src/vs/workbench/api/common/extHostTypes/notebooks.ts b/src/vs/workbench/api/common/extHostTypes/notebooks.ts index 3c8e906022395..9eee022cdf1cd 100644 --- a/src/vs/workbench/api/common/extHostTypes/notebooks.ts +++ b/src/vs/workbench/api/common/extHostTypes/notebooks.ts @@ -9,8 +9,6 @@ import { illegalArgument } from '../../../../base/common/errors.js'; import { Mimes, normalizeMimeType, isTextStreamMime } from '../../../../base/common/mime.js'; import { generateUuid } from '../../../../base/common/uuid.js'; -/* eslint-disable local/code-no-native-private */ - export enum NotebookCellKind { Markup = 1, Code = 2 diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 435df4b03fc93..3c66bc3eda68b 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { VSBuffer } from '../../../base/common/buffer.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts index d9059d6896b54..4f9bac1451ac9 100644 --- a/src/vs/workbench/api/common/extHostWebviewPanels.ts +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { Emitter } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts index 4696f33c5fae8..322f9c1ca3f7a 100644 --- a/src/vs/workbench/api/common/extHostWebviewView.ts +++ b/src/vs/workbench/api/common/extHostWebviewView.ts @@ -13,8 +13,6 @@ import type * as vscode from 'vscode'; import * as extHostProtocol from './extHost.protocol.js'; import * as extHostTypes from './extHostTypes.js'; -/* eslint-disable local/code-no-native-private */ - class ExtHostWebviewView extends Disposable implements vscode.WebviewView { readonly #handle: extHostProtocol.WebviewHandle; From df73a31bda12f28bd84abf3adaa2824d8491abc4 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 16 Feb 2026 10:59:41 +0100 Subject: [PATCH 12/12] Adding unit test for insertedLines array (#295541) adding unit test for inserted lines --- .../common/viewLayout/lineHeights.test.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts index c94726913baa3..113083e038a4f 100644 --- a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { LineHeightsManager } from '../../../common/viewLayout/lineHeights.js'; +import { CustomLineHeightData, LineHeightsManager } from '../../../common/viewLayout/lineHeights.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; suite('Editor ViewLayout - LineHeightsManager', () => { @@ -276,4 +276,26 @@ suite('Editor ViewLayout - LineHeightsManager', () => { assert.strictEqual(manager.heightForLineNumber(5), 40); assert.strictEqual(manager.heightForLineNumber(6), 30); }); + + test('onLinesInserted with same decoration ID extending to inserted line', () => { + const manager = new LineHeightsManager(10, []); + // Set up a special line at line 1 with decoration 'decA' + manager.insertOrChangeCustomLineHeight('decA', 1, 1, 30); + manager.commit(); + + assert.strictEqual(manager.heightForLineNumber(1), 30); + assert.strictEqual(manager.heightForLineNumber(2), 10); + + // Insert line 2 to line 2, with the same decoration ID 'decA' covering line 2 + manager.onLinesInserted(2, 2, [ + new CustomLineHeightData('decA', 2, 2, 30) + ]); + + // After insertion, the decoration 'decA' now covers line 2 + // Since insertOrChangeCustomLineHeight removes the old decoration first, + // line 1 no longer has the custom height, and line 2 gets it + assert.strictEqual(manager.heightForLineNumber(1), 10); + assert.strictEqual(manager.heightForLineNumber(2), 30); + assert.strictEqual(manager.heightForLineNumber(3), 10); + }); });