diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0b3388f9aeb05..9d23fc89077a0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,3 +17,4 @@ build/lib/policies/policyData.jsonc @joshspicer @rebornix @joaomoreno @pwang347 # Ensure the API team is aware of changes to the vscode-dts file # this is only about the final API, not about proposed API changes src/vscode-dts/vscode.d.ts @jrieken @mjbvz @alexr00 +src/vs/workbench/services/extensions/common/extensionPoints.json @jrieken @mjbvz @alexr00 diff --git a/build/azure-pipelines/product-sanity-tests.yml b/build/azure-pipelines/product-sanity-tests.yml index f334ff3ed7505..9a612b43ee283 100644 --- a/build/azure-pipelines/product-sanity-tests.yml +++ b/build/azure-pipelines/product-sanity-tests.yml @@ -15,6 +15,7 @@ parameters: - name: buildCommit displayName: Published Build Commit type: string + default: '' - name: npmRegistry displayName: Custom NPM Registry URL @@ -27,9 +28,17 @@ variables: - name: Codeql.SkipTaskAutoInjection value: true - name: BUILD_COMMIT - value: ${{ parameters.buildCommit }} + ${{ if ne(parameters.buildCommit, '') }}: + value: ${{ parameters.buildCommit }} + ${{ else }}: + value: $(resources.pipeline.vscode.sourceCommit) - name: BUILD_QUALITY - value: ${{ parameters.buildQuality }} + ${{ if ne(parameters.buildCommit, '') }}: + value: ${{ parameters.buildQuality }} + ${{ elseif startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') }}: + value: stable + ${{ else }}: + value: insider - name: NPM_REGISTRY value: ${{ parameters.npmRegistry }} @@ -41,6 +50,17 @@ resources: type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release + pipelines: + - pipeline: vscode + # allow-any-unicode-next-line + source: '⭐️ VS Code' + trigger: + stages: + - Publish + branches: + include: + - main + - release/* extends: template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts index 27149338d9f7e..b935764033e55 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -321,7 +321,7 @@ function packageTask(type: string, platform: string, arch: string, sourceFolderN let productJsonContents = ''; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(jsonEditor({ commit, date: readISODate('out-build'), version })) + .pipe(jsonEditor({ commit, date: readISODate(sourceFolderName), version })) .pipe(es.through(function (file) { productJsonContents = file.contents.toString(); this.emit('data', file); diff --git a/build/gulpfile.ts b/build/gulpfile.ts index 1c21618ca608f..a3e6a82d5728f 100644 --- a/build/gulpfile.ts +++ b/build/gulpfile.ts @@ -15,6 +15,9 @@ import * as task from './lib/task.ts'; import * as util from './lib/util.ts'; import { useEsbuildTranspile } from './buildConfig.ts'; +// Extension point names +gulp.task(compilation.compileExtensionPointNamesTask); + const require = createRequire(import.meta.url); // API proposal names @@ -30,12 +33,12 @@ const transpileClientTask = task.define('transpile-client', task.series(util.rim gulp.task(transpileClientTask); // Fast compile for development time -const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compilation.compileApiProposalNamesTask, compilation.compileTask('src', 'out', false))); +const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compilation.compileApiProposalNamesTask, compilation.compileExtensionPointNamesTask, compilation.compileTask('src', 'out', false))); gulp.task(compileClientTask); const watchClientTask = useEsbuildTranspile - ? task.define('watch-client', task.parallel(compilation.watchTask('out', false, 'src', { noEmit: true }), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask)) - : task.define('watch-client', task.series(util.rimraf('out'), task.parallel(compilation.watchTask('out', false), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask))); + ? task.define('watch-client', task.parallel(compilation.watchTask('out', false, 'src', { noEmit: true }), compilation.watchApiProposalNamesTask, compilation.watchExtensionPointNamesTask, compilation.watchCodiconsTask)) + : task.define('watch-client', task.series(util.rimraf('out'), task.parallel(compilation.watchTask('out', false), compilation.watchApiProposalNamesTask, compilation.watchExtensionPointNamesTask, compilation.watchCodiconsTask))); gulp.task(watchClientTask); // All diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 19504aaf7c799..ea4f88df500d0 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -371,7 +371,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d let productJsonContents: string; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(jsonEditor({ commit, date: readISODate('out-build'), checksums, version })) + .pipe(jsonEditor({ commit, date: readISODate(out), checksums, version })) .pipe(es.through(function (file) { productJsonContents = file.contents.toString(); this.emit('data', file); diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 2484e884a5512..0d6209e66c862 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -25,6 +25,9 @@ import * as tsb from './tsb/index.ts'; import sourcemaps from 'gulp-sourcemaps'; +import { extractExtensionPointNamesFromFile } from './extractExtensionPoints.ts'; + + // --- gulp-tsb: compile and transpile -------------------------------- const reporter = createReporter(); @@ -351,6 +354,49 @@ export const compileApiProposalNamesTask = task.define('compile-api-proposal-nam .pipe(apiProposalNamesReporter.end(true)); }); +function generateExtensionPointNames() { + const collectedNames: string[] = []; + + const input = es.through(); + const output = input + .pipe(es.through(function (file: File) { + const contents = file.contents?.toString('utf-8'); + if (contents && contents.includes('registerExtensionPoint')) { + const sourceFile = ts.createSourceFile(file.path, contents, ts.ScriptTarget.Latest, true); + collectedNames.push(...extractExtensionPointNamesFromFile(sourceFile)); + } + }, function () { + collectedNames.sort(); + const content = JSON.stringify(collectedNames, undefined, '\t') + '\n'; + this.emit('data', new File({ + path: 'vs/workbench/services/extensions/common/extensionPoints.json', + contents: Buffer.from(content) + })); + this.emit('end'); + })); + + return es.duplex(input, output); +} + +const extensionPointNamesReporter = createReporter('extension-point-names'); + +export const compileExtensionPointNamesTask = task.define('compile-extension-point-names', () => { + return gulp.src('src/vs/workbench/**/*.ts') + .pipe(generateExtensionPointNames()) + .pipe(gulp.dest('src')) + .pipe(extensionPointNamesReporter.end(true)); +}); + +export const watchExtensionPointNamesTask = task.define('watch-extension-point-names', () => { + const task = () => gulp.src('src/vs/workbench/**/*.ts') + .pipe(generateExtensionPointNames()) + .pipe(extensionPointNamesReporter.end(true)); + + return watch('src/vs/workbench/**/*.ts', { readDelay: 200 }) + .pipe(util.debounce(task)) + .pipe(gulp.dest('src')); +}); + export const watchApiProposalNamesTask = task.define('watch-api-proposal-names', () => { const task = () => gulp.src('src/vscode-dts/**') .pipe(generateApiProposalNames()) diff --git a/build/lib/date.ts b/build/lib/date.ts index 9c20c9eeb22ab..68d52521349ed 100644 --- a/build/lib/date.ts +++ b/build/lib/date.ts @@ -29,5 +29,13 @@ export function writeISODate(outDir: string) { export function readISODate(outDir: string): string { const outDirectory = path.join(root, outDir); - return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8'); + try { + return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8'); + } catch { + // Fallback to out-build (old build writes date there, esbuild writes to bundle output dir) + if (outDir !== 'out-build') { + return fs.readFileSync(path.join(root, 'out-build', 'date'), 'utf8'); + } + throw new Error(`Could not find date file in ${outDir}`); + } } diff --git a/build/lib/extractExtensionPoints.ts b/build/lib/extractExtensionPoints.ts new file mode 100644 index 0000000000000..1c442564026b3 --- /dev/null +++ b/build/lib/extractExtensionPoints.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Extracts extension point names from TypeScript source files by parsing the AST + * to find all calls to `ExtensionsRegistry.registerExtensionPoint(...)`. + * + * Handles: + * - Inline string literals: `{ extensionPoint: 'foo' }` + * - Enum member values passed via function parameters + * - Imported descriptor variables where the `extensionPoint` property is in another file + * + * This module can be used standalone (`node build/lib/extractExtensionPoints.ts`) + * to regenerate the extension points file, or imported for use in gulp build tasks. + */ + +import ts from 'typescript'; +import path from 'path'; +import fs from 'fs'; + +/** + * Extract extension point names registered via `registerExtensionPoint` from + * a single TypeScript source file's AST. No type checker is needed. + */ +export function extractExtensionPointNamesFromFile(sourceFile: ts.SourceFile): string[] { + const results: string[] = []; + visit(sourceFile); + return results; + + function visit(node: ts.Node): void { + if (ts.isCallExpression(node)) { + const expr = node.expression; + if (ts.isPropertyAccessExpression(expr) && expr.name.text === 'registerExtensionPoint') { + handleRegisterCall(node); + } + } + ts.forEachChild(node, visit); + } + + function handleRegisterCall(call: ts.CallExpression): void { + const arg = call.arguments[0]; + if (!arg) { + return; + } + if (ts.isObjectLiteralExpression(arg)) { + handleInlineDescriptor(call, arg); + } else if (ts.isIdentifier(arg)) { + handleImportedDescriptor(arg); + } + } + + function handleInlineDescriptor(call: ts.CallExpression, obj: ts.ObjectLiteralExpression): void { + const epProp = findExtensionPointProperty(obj); + if (!epProp) { + return; + } + if (ts.isStringLiteral(epProp.initializer)) { + results.push(epProp.initializer.text); + } else if (ts.isIdentifier(epProp.initializer)) { + // The value references a function parameter - resolve via call sites + handleParameterReference(call, epProp.initializer.text); + } + } + + function handleParameterReference(registerCall: ts.CallExpression, paramName: string): void { + // Walk up to find the containing function + let current: ts.Node | undefined = registerCall.parent; + while (current && !ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) { + current = current.parent; + } + if (!current) { + return; + } + const fn = current as ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction; + + // Find which parameter position matches paramName + const paramIndex = fn.parameters.findIndex( + p => ts.isIdentifier(p.name) && p.name.text === paramName + ); + if (paramIndex < 0) { + return; + } + + // Find the function name to locate call sites + const fnName = ts.isFunctionDeclaration(fn) && fn.name ? fn.name.text : undefined; + if (!fnName) { + return; + } + + // Find all call sites of this function in the same file + ts.forEachChild(sourceFile, function findCalls(node) { + if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === fnName) { + const callArg = node.arguments[paramIndex]; + if (callArg) { + const value = resolveStringValue(callArg); + if (value) { + results.push(value); + } + } + } + ts.forEachChild(node, findCalls); + }); + } + + function handleImportedDescriptor(identifier: ts.Identifier): void { + const name = identifier.text; + for (const stmt of sourceFile.statements) { + if (!ts.isImportDeclaration(stmt) || !stmt.importClause?.namedBindings) { + continue; + } + if (!ts.isNamedImports(stmt.importClause.namedBindings)) { + continue; + } + for (const element of stmt.importClause.namedBindings.elements) { + if (element.name.text !== name || !ts.isStringLiteral(stmt.moduleSpecifier)) { + continue; + } + const modulePath = stmt.moduleSpecifier.text; + const resolvedPath = path.resolve( + path.dirname(sourceFile.fileName), + modulePath.replace(/\.js$/, '.ts') + ); + try { + const content = fs.readFileSync(resolvedPath, 'utf-8'); + const importedFile = ts.createSourceFile(resolvedPath, content, ts.ScriptTarget.Latest, true); + const originalName = element.propertyName?.text || element.name.text; + const value = findExtensionPointInVariable(importedFile, originalName); + if (value) { + results.push(value); + } + } catch { + // Imported file not found, skip + } + return; + } + } + } + + function resolveStringValue(node: ts.Node): string | undefined { + if (ts.isStringLiteral(node)) { + return node.text; + } + // Property access: Enum.Member + if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) { + const enumName = node.expression.text; + const memberName = node.name.text; + for (const stmt of sourceFile.statements) { + if (ts.isEnumDeclaration(stmt) && stmt.name.text === enumName) { + for (const member of stmt.members) { + if (ts.isIdentifier(member.name) && member.name.text === memberName + && member.initializer && ts.isStringLiteral(member.initializer)) { + return member.initializer.text; + } + } + } + } + } + return undefined; + } +} + +function findExtensionPointProperty(obj: ts.ObjectLiteralExpression): ts.PropertyAssignment | undefined { + for (const prop of obj.properties) { + if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'extensionPoint') { + return prop; + } + } + return undefined; +} + +function findExtensionPointInVariable(sourceFile: ts.SourceFile, varName: string): string | undefined { + for (const stmt of sourceFile.statements) { + if (!ts.isVariableStatement(stmt)) { + continue; + } + for (const decl of stmt.declarationList.declarations) { + if (ts.isIdentifier(decl.name) && decl.name.text === varName + && decl.initializer && ts.isObjectLiteralExpression(decl.initializer)) { + const epProp = findExtensionPointProperty(decl.initializer); + if (epProp && ts.isStringLiteral(epProp.initializer)) { + return epProp.initializer.text; + } + } + } + } + return undefined; +} + +// --- Standalone CLI --- + +const rootDir = path.resolve(import.meta.dirname, '..', '..'); +const srcDir = path.join(rootDir, 'src'); +const outputPath = path.join(srcDir, 'vs', 'workbench', 'services', 'extensions', 'common', 'extensionPoints.json'); + +function scanDirectory(dir: string): string[] { + const names: string[] = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + names.push(...scanDirectory(fullPath)); + } else if (entry.name.endsWith('.ts')) { + const content = fs.readFileSync(fullPath, 'utf-8'); + if (content.includes('registerExtensionPoint')) { + const sourceFile = ts.createSourceFile(fullPath, content, ts.ScriptTarget.Latest, true); + names.push(...extractExtensionPointNamesFromFile(sourceFile)); + } + } + } + return names; +} + +function main(): void { + const names = scanDirectory(path.join(srcDir, 'vs', 'workbench')); + names.sort(); + const output = JSON.stringify(names, undefined, '\t') + '\n'; + fs.writeFileSync(outputPath, output, 'utf-8'); + console.log(`Wrote ${names.length} extension points to ${path.relative(rootDir, outputPath)}`); +} + +if (import.meta.main) { + main(); +} diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index da9a664e813fe..47bff4e1d83cb 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -354,13 +354,9 @@ class MainThreadChatSessionItemController extends Disposable implements IChatSes this._onDidChangeChatSessionItems.fire(); } - updateItem(item: IChatSessionItem): void { - if (this._items.has(item.resource)) { - this._items.set(item.resource, item); - this._onDidChangeChatSessionItems.fire(); - } else { - console.warn(`Item with resource ${item.resource.toString()} does not exist. Skipping update.`); - } + addOrUpdateItem(item: IChatSessionItem): void { + this._items.set(item.resource, item); + this._onDidChangeChatSessionItems.fire(); } fireOnDidChangeChatSessionItems(): void { @@ -440,8 +436,17 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat )); } + private getController(handle: number): MainThreadChatSessionItemController { + const registration = this._itemControllerRegistrations.get(handle); + if (!registration) { + throw new Error(`No chat session controller registered for handle ${handle}`); + } + return registration.controller; + } + $onDidChangeChatSessionItems(handle: number): void { - this._itemControllerRegistrations.get(handle)?.controller.fireOnDidChangeChatSessionItems(); + const controller = this.getController(handle); + controller.fireOnDidChangeChatSessionItems(); } private async _resolveSessionItem(item: Dto): Promise { @@ -474,31 +479,20 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat }; } - async $setChatSessionItems(handle: number, items: Dto[]): Promise { - const registration = this._itemControllerRegistrations.get(handle); - if (!registration) { - this._logService.warn(`No chat session controller registered for handle ${handle}`); - return; - } - + async $setChatSessionItems(controllerHandle: number, items: Dto[]): Promise { + const controller = this.getController(controllerHandle); const resolvedItems = await Promise.all(items.map(item => this._resolveSessionItem(item))); - registration.controller.setItems(resolvedItems); + controller.setItems(resolvedItems); } - async $updateChatSessionItem(controllerHandle: number, item: Dto): Promise { - const registration = this._itemControllerRegistrations.get(controllerHandle); - if (!registration) { - this._logService.warn(`No chat session controller registered for handle ${controllerHandle}`); - return; - } - + async $addOrUpdateChatSessionItem(controllerHandle: number, item: Dto): Promise { + const controller = this.getController(controllerHandle); const resolvedItem = await this._resolveSessionItem(item); - registration.controller.updateItem(resolvedItem); + controller.addOrUpdateItem(resolvedItem); } $onDidChangeChatSessionOptions(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string }>): void { const sessionResource = URI.revive(sessionResourceComponents); - this._chatSessionsService.notifySessionOptionsChange(sessionResource, updates); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 422aa1b582774..d12f99c005167 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3418,7 +3418,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { $registerChatSessionItemController(handle: number, chatSessionType: string): void; $unregisterChatSessionItemController(handle: number): void; $setChatSessionItems(handle: number, items: Dto[]): Promise; - $updateChatSessionItem(handle: number, item: Dto): Promise; + $addOrUpdateChatSessionItem(handle: number, item: Dto): Promise; $onDidChangeChatSessionItems(handle: number): void; $onDidCommitChatSessionItem(handle: number, original: UriComponents, modified: UriComponents): void; $registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void; diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index cec3c62b01574..8868ad6611017 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -176,12 +176,17 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } } +interface SessionCollectionListeners { + onItemsChanged(): void; + onItemAddedOrUpdated(item: vscode.ChatSessionItem): void; +} + class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection { readonly #items = new ResourceMap(); - #onItemsChanged: () => void; + readonly #callbacks: SessionCollectionListeners; - constructor(onItemsChanged: () => void) { - this.#onItemsChanged = onItemsChanged; + constructor(callbacks: SessionCollectionListeners) { + this.#callbacks = callbacks; } get size(): number { @@ -197,7 +202,7 @@ class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection for (const item of items) { this.#items.set(item.resource, item); } - this.#onItemsChanged(); + this.#callbacks.onItemsChanged(); } forEach(callback: (item: vscode.ChatSessionItem, collection: vscode.ChatSessionItemCollection) => unknown, thisArg?: any): void { @@ -207,13 +212,20 @@ class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection } add(item: vscode.ChatSessionItem): void { + const existing = this.#items.get(item.resource); + if (existing && existing === item) { + // We're adding the same item again + return; + } + this.#items.set(item.resource, item); - this.#onItemsChanged(); + this.#callbacks.onItemAddedOrUpdated(item); } delete(resource: vscode.Uri): void { - this.#items.delete(resource); - this.#onItemsChanged(); + if (this.#items.delete(resource)) { + this.#callbacks.onItemsChanged(); + } } get(resource: vscode.Uri): vscode.ChatSessionItem | undefined { @@ -324,8 +336,10 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio const onDidChangeChatSessionItemStateEmitter = disposables.add(new Emitter()); - const collection = new ChatSessionItemCollectionImpl(() => { + const collection = new ChatSessionItemCollectionImpl({ // Noop for providers + onItemsChanged: () => { }, + onItemAddedOrUpdated: () => { } }); // Helper to push items to main thread @@ -400,7 +414,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio void this._proxy.$setChatSessionItems(controllerHandle, items); }; - const collection = new ChatSessionItemCollectionImpl(onItemsChanged); + const collection = new ChatSessionItemCollectionImpl({ + onItemsChanged, + onItemAddedOrUpdated: (item: vscode.ChatSessionItem) => { + void this._proxy.$addOrUpdateChatSessionItem(controllerHandle, typeConvert.ChatSessionItem.from(item)); + } + }); const controller = Object.freeze({ id, @@ -420,7 +439,10 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } const item = new ChatSessionItemImpl(resource, label, () => { - void this._proxy.$updateChatSessionItem(controllerHandle, typeConvert.ChatSessionItem.from(item)); + // Make sure the item really is in the collection. If not we don't need to transmit it to the main thread yet + if (collection.get(resource) === item) { + void this._proxy.$addOrUpdateChatSessionItem(controllerHandle, typeConvert.ChatSessionItem.from(item)); + } }); return item; }, diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 97a74fcf8fa02..c7679ab51cb73 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -22,7 +22,7 @@ import { getMenuBarVisibility, IPath, hasNativeTitlebar, hasCustomTitlebar, Titl import { IHostService } from '../services/host/browser/host.js'; import { IBrowserWorkbenchEnvironmentService } from '../services/environment/browser/environmentService.js'; import { IEditorService } from '../services/editor/common/editorService.js'; -import { EditorGroupLayout, GroupOrientation, GroupsOrder, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; +import { EditorGroupLayout, GroupActivationReason, GroupOrientation, GroupsOrder, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; import { SerializableGrid, ISerializableView, ISerializedGrid, Orientation, ISerializedNode, ISerializedLeafNode, Direction, IViewSize, Sizing, IViewVisibilityAnimationOptions } from '../../base/browser/ui/grid/grid.js'; import { Part } from './part.js'; import { IStatusbarService } from '../services/statusbar/browser/statusbar.js'; @@ -382,7 +382,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi showEditorIfHidden(); } })); - this._register(this.editorGroupService.mainPart.onDidActivateGroup(showEditorIfHidden)); + this._register(this.editorGroupService.mainPart.onDidActivateGroup(e => { + if (e.reason !== GroupActivationReason.PART_CLOSE) { + showEditorIfHidden(); // only show unless a modal/auxiliary part closes + } + })); // Revalidate center layout when active editor changes: diff editor quits centered mode this._register(this.mainPartEditorService.onDidActiveEditorChange(() => this.centerMainEditorLayout(this.stateModel.getRuntimeValue(LayoutStateKeys.MAIN_EDITOR_CENTERED)))); diff --git a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts index ec1cf50a7be5a..327b8bd85dcbe 100644 --- a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts @@ -5,6 +5,7 @@ import { onDidChangeFullscreen } from '../../../../base/browser/browser.js'; import { $, getActiveWindow, hide, show } from '../../../../base/browser/dom.js'; +import { mainWindow } from '../../../../base/browser/window.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, markAsSingleton, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { isNative } from '../../../../base/common/platform.js'; @@ -20,10 +21,10 @@ import { EditorPart, IEditorPartUIState } from './editorPart.js'; import { IAuxiliaryTitlebarPart } from '../titlebar/titlebarPart.js'; import { WindowTitle } from '../titlebar/windowTitle.js'; import { IAuxiliaryWindowOpenOptions, IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; -import { GroupDirection, GroupsOrder, IAuxiliaryEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { GroupDirection, GroupsOrder, IAuxiliaryEditorPart, GroupActivationReason } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IHostService } from '../../../services/host/browser/host.js'; -import { IWorkbenchLayoutService, shouldShowCustomTitleBar } from '../../../services/layout/browser/layoutService.js'; +import { IWorkbenchLayoutService, Parts, shouldShowCustomTitleBar } from '../../../services/layout/browser/layoutService.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { IStatusbarService } from '../../../services/statusbar/browser/statusbar.js'; import { ITitleService } from '../../../services/title/browser/titleService.js'; @@ -409,13 +410,19 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart private doRemoveLastGroup(preserveFocus?: boolean): void { const restoreFocus = !preserveFocus && this.shouldRestoreFocus(this.container); - // Activate next group + // Activate next group when closing const mostRecentlyActiveGroups = this.editorPartsView.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); const nextActiveGroup = mostRecentlyActiveGroups[1]; // [0] will be the current group we are about to dispose if (nextActiveGroup) { - nextActiveGroup.groupsView.activateGroup(nextActiveGroup); + nextActiveGroup.groupsView.activateGroup(nextActiveGroup, undefined, GroupActivationReason.PART_CLOSE); + } - if (restoreFocus) { + // Deal with focus: focus the next recently used group but skip + // this if the next group is in the main part and the main part + // is currently hidden, as that would make it visible. + if (nextActiveGroup && restoreFocus) { + const nextGroupInHiddenMainPart = nextActiveGroup.groupsView === this.editorPartsView.mainPart && !this.layoutService.isVisible(Parts.EDITOR_PART, mainWindow); + if (!nextGroupInHiddenMainPart) { nextActiveGroup.focus(); } } diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 9677912440325..9a1b9ab092fd1 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -5,7 +5,7 @@ import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane, IEditorPartLimitOptions, IEditorPartDecorationOptions, IEditorWillOpenEvent, EditorInputWithOptions } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; -import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement, IAuxiliaryEditorPart, IEditorPart, IModalEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement, IAuxiliaryEditorPart, IEditorPart, IModalEditorPart, GroupActivationReason } from '../../../services/editor/common/editorGroupsService.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { Dimension } from '../../../../base/browser/dom.js'; import { Event } from '../../../../base/common/event.js'; @@ -218,7 +218,7 @@ export interface IEditorGroupsView { getGroup(identifier: GroupIdentifier): IEditorGroupView | undefined; getGroups(order: GroupsOrder): IEditorGroupView[]; - activateGroup(identifier: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean): IEditorGroupView; + activateGroup(identifier: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean, reason?: GroupActivationReason): IEditorGroupView; restoreGroup(identifier: IEditorGroupView | GroupIdentifier): IEditorGroupView; addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection, groupToCopy?: IEditorGroupView): IEditorGroupView; diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 56ca88b06d6b0..4326662a9abaa 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -8,7 +8,7 @@ import { Part } from '../../part.js'; import { Dimension, $, EventHelper, addDisposableGenericMouseDownListener, getWindow, isAncestorOfActiveElement, getActiveElement, isHTMLElement } from '../../../../base/browser/dom.js'; import { Event, Emitter, Relay, PauseableEmitter } from '../../../../base/common/event.js'; import { contrastBorder, editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; -import { GroupDirection, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, GroupsOrder, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorSideGroup, IEditorDropTargetDelegate, IEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { GroupDirection, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, GroupsOrder, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorSideGroup, IEditorDropTargetDelegate, IEditorPart, GroupActivationReason, IEditorGroupActivationEvent } from '../../../services/editor/common/editorGroupsService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGrid, Sizing, ISerializedGrid, ISerializedNode, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from '../../../../base/browser/ui/grid/grid.js'; import { GroupIdentifier, EditorInputWithOptions, IEditorPartOptions, IEditorPartOptionsChangeEvent, GroupModelChangeKind } from '../../../common/editor.js'; @@ -116,7 +116,7 @@ export class EditorPart extends Part implements IEditorPart, private readonly _onDidChangeGroupMaximized = this._register(new Emitter()); readonly onDidChangeGroupMaximized = this._onDidChangeGroupMaximized.event; - private readonly _onDidActivateGroup = this._register(new Emitter()); + private readonly _onDidActivateGroup = this._register(new Emitter()); readonly onDidActivateGroup = this._onDidActivateGroup.event; private readonly _onDidAddGroup = this._register(new PauseableEmitter()); @@ -365,9 +365,9 @@ export class EditorPart extends Part implements IEditorPart, } } - activateGroup(group: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean): IEditorGroupView { + activateGroup(group: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean, reason?: GroupActivationReason): IEditorGroupView { const groupView = this.assertGroupView(group); - this.doSetGroupActive(groupView); + this.doSetGroupActive(groupView, reason); // Ensure window on top unless disabled if (!preserveWindowOrder) { @@ -684,7 +684,7 @@ export class EditorPart extends Part implements IEditorPart, return groupView; } - private doSetGroupActive(group: IEditorGroupView): void { + private doSetGroupActive(group: IEditorGroupView, reason = GroupActivationReason.DEFAULT): void { if (this._activeGroup !== group) { const previousActiveGroup = this._activeGroup; this._activeGroup = group; @@ -710,7 +710,7 @@ export class EditorPart extends Part implements IEditorPart, // Always fire the event that a group has been activated // even if its the same group that is already active to // signal the intent even when nothing has changed. - this._onDidActivateGroup.fire(group); + this._onDidActivateGroup.fire({ group, reason }); } private doRestoreGroup(group: IEditorGroupView): void { diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 61703e493fdd6..8565198960330 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions, IEditorPart, IModalEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions, IEditorPart, IModalEditorPart, IEditorGroupActivationEvent } from '../../../services/editor/common/editorGroupsService.js'; import { Emitter } from '../../../../base/common/event.js'; import { DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { GroupIdentifier, IEditorPartOptions } from '../../../common/editor.js'; @@ -234,7 +234,7 @@ export class EditorParts extends MultiWindowParts this._onDidAddGroup.fire(group))); disposables.add(part.onDidRemoveGroup(group => this._onDidRemoveGroup.fire(group))); disposables.add(part.onDidMoveGroup(group => this._onDidMoveGroup.fire(group))); - disposables.add(part.onDidActivateGroup(group => this._onDidActivateGroup.fire(group))); + disposables.add(part.onDidActivateGroup(e => this._onDidActivateGroup.fire(e))); disposables.add(part.onDidChangeGroupMaximized(maximized => this._onDidChangeGroupMaximized.fire(maximized))); disposables.add(part.onDidChangeGroupIndex(group => this._onDidChangeGroupIndex.fire(group))); @@ -558,7 +558,7 @@ export class EditorParts extends MultiWindowParts()); readonly onDidMoveGroup = this._onDidMoveGroup.event; - private readonly _onDidActivateGroup = this._register(new Emitter()); + private readonly _onDidActivateGroup = this._register(new Emitter()); readonly onDidActivateGroup = this._onDidActivateGroup.event; private readonly _onDidChangeGroupIndex = this._register(new Emitter()); diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 6e65299f40088..8d2c6acb5920e 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './media/modalEditorPart.css'; -import { $, addDisposableListener, append, EventHelper, EventType } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventHelper, EventType, isHTMLElement } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -20,12 +20,12 @@ import { IStorageService } from '../../../../platform/storage/common/storage.js' import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IEditorGroupView, IEditorPartsView } from './editor.js'; import { EditorPart } from './editorPart.js'; -import { GroupDirection, GroupsOrder, IModalEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { GroupDirection, GroupsOrder, IModalEditorPart, GroupActivationReason } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { EditorPartModalContext, EditorPartModalMaximizedContext } from '../../../common/contextkeys.js'; import { Verbosity } from '../../../common/editor.js'; import { IHostService } from '../../../services/host/browser/host.js'; -import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; +import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { localize } from '../../../../nls.js'; @@ -208,6 +208,8 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { private readonly optionsDisposable = this._register(new MutableDisposable()); + private previousMainWindowActiveElement: Element | null = null; + constructor( windowId: number, editorPartsView: IEditorPartsView, @@ -226,6 +228,12 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { this.enforceModalPartOptions(); } + override create(parent: HTMLElement, options?: object): void { + this.previousMainWindowActiveElement = mainWindow.document.activeElement; + + super.create(parent, options); + } + private enforceModalPartOptions(): void { const editorCount = this.groups.reduce((count, group) => count + group.count, 0); this.optionsDisposable.value = this.enforcePartOptions({ @@ -264,7 +272,7 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { // Close modal when last group removed const groupView = this.assertGroupView(group); if (this.count === 1 && this.activeGroup === groupView) { - this.doRemoveLastGroup(preserveFocus); + this.doRemoveLastGroup(); } // Otherwise delegate to parent implementation @@ -273,18 +281,26 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { } } - private doRemoveLastGroup(preserveFocus?: boolean): void { - const restoreFocus = !preserveFocus && this.shouldRestoreFocus(this.container); - - // Activate next group - const mostRecentlyActiveGroups = this.editorPartsView.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); - const nextActiveGroup = mostRecentlyActiveGroups[1]; // [0] will be the current group we are about to dispose - if (nextActiveGroup) { - nextActiveGroup.groupsView.activateGroup(nextActiveGroup); - - if (restoreFocus) { - nextActiveGroup.focus(); - } + private doRemoveLastGroup(): void { + + // Activate main editor group when closing + const activeMainGroup = this.editorPartsView.mainPart.activeGroup; + this.editorPartsView.mainPart.activateGroup(activeMainGroup, undefined, GroupActivationReason.PART_CLOSE); + + // Deal with focus: removing the last modal group + // means we return back to the main editor part. + // But we only want to focus that if it was focused + // before to prevent revealing the editor part if + // it was maybe hidden before. + const mainEditorPartContainer = this.layoutService.getContainer(mainWindow, Parts.EDITOR_PART); + if ( + !isHTMLElement(this.previousMainWindowActiveElement) || // invalid previous element + !this.previousMainWindowActiveElement.isConnected || // previous element no longer in the DOM + mainEditorPartContainer?.contains(this.previousMainWindowActiveElement) // previous element is inside main editor part + ) { + activeMainGroup.focus(); + } else { + this.previousMainWindowActiveElement.focus(); } this.doClose({ mergeConfirmingEditorsToMainPart: false }); diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index b55b824cd7d65..5740a3345c789 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -437,15 +437,14 @@ export class ChatService extends Disposable implements IChatService { initialData: undefined, location, sessionResource, - sessionId, canUseTools: options?.canUseTools ?? true, disableBackgroundKeepAlive: options?.disableBackgroundKeepAlive }); } private _startSession(props: IStartSessionProps): ChatModel { - const { initialData, location, sessionResource, sessionId, canUseTools, transferEditingSession, disableBackgroundKeepAlive, inputState } = props; - const model = this.instantiationService.createInstance(ChatModel, initialData, { initialLocation: location, canUseTools, resource: sessionResource, sessionId, disableBackgroundKeepAlive, inputState }); + const { initialData, location, sessionResource, canUseTools, transferEditingSession, disableBackgroundKeepAlive, inputState } = props; + const model = this.instantiationService.createInstance(ChatModel, initialData, { initialLocation: location, canUseTools, resource: sessionResource, disableBackgroundKeepAlive, inputState }); if (location === ChatAgentLocation.Chat) { model.startEditingSession(true, transferEditingSession); } @@ -503,17 +502,17 @@ export class ChatService extends Disposable implements IChatService { return existingRef; } - const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); - if (!sessionId) { - throw new Error(`Cannot restore non-local session ${sessionResource}`); - } - let sessionData: ISerializedChatDataReference | undefined; if (isEqual(this.transferredSessionResource, sessionResource)) { this._transferredSessionResource = undefined; sessionData = await this._chatSessionStore.readTransferredSession(sessionResource); } else { - sessionData = await this._chatSessionStore.readSession(sessionId); + const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (localSessionId) { + sessionData = await this._chatSessionStore.readSession(localSessionId); + } else { + return this.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + } } if (!sessionData) { @@ -524,7 +523,6 @@ export class ChatService extends Disposable implements IChatService { initialData: sessionData, location: sessionData.value.initialLocation ?? ChatAgentLocation.Chat, sessionResource, - sessionId, canUseTools: true, }); @@ -550,7 +548,6 @@ export class ChatService extends Disposable implements IChatService { initialData: { value: data, serializer: new ChatSessionOperationLog() }, location: data.initialLocation ?? ChatAgentLocation.Chat, sessionResource, - sessionId, canUseTools: true, }); } @@ -568,6 +565,14 @@ export class ChatService extends Disposable implements IChatService { } const providedSession = await this.chatSessionService.getOrCreateChatSession(chatSessionResource, CancellationToken.None); + + // Make sure we haven't created this in the meantime + const existingRefAfterProvision = this._sessionModels.acquireExisting(chatSessionResource); + if (existingRefAfterProvision) { + providedSession.dispose(); + return existingRefAfterProvision; + } + const chatSessionType = chatSessionResource.scheme; // Contributed sessions do not use UI tools diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 7f9170a04f2b0..3b741cb825a9e 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -37,7 +37,7 @@ import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../languageModels.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, UserSelectedTools, reviveSerializedAgent } from '../participants/chatAgents.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from '../requestParser/chatParserTypes.js'; -import { LocalChatSessionUri } from './chatUri.js'; +import { chatSessionResourceToId, LocalChatSessionUri } from './chatUri.js'; import { ObjectMutationLog } from './objectMutationLog.js'; @@ -2102,7 +2102,7 @@ export class ChatModel extends Disposable implements IChatModel { constructor( dataRef: ISerializedChatDataReference | undefined, - initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; sessionId?: string; disableBackgroundKeepAlive?: boolean }, + initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; disableBackgroundKeepAlive?: boolean }, @ILogService private readonly logService: ILogService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IChatEditingService private readonly chatEditingService: IChatEditingService, @@ -2118,8 +2118,23 @@ export class ChatModel extends Disposable implements IChatModel { } this._isImported = !!initialData && isValidExportedData && !isValidFullData; - this._sessionId = (isValidFullData && initialData.sessionId) || initialModelProps.sessionId || generateUuid(); - this._sessionResource = initialModelProps.resource ?? LocalChatSessionUri.forSession(this._sessionId); + + // Set the session resource and id + if (initialModelProps.resource) { + // prefer using the provided resource if provided + this._sessionId = chatSessionResourceToId(initialModelProps.resource); + this._sessionResource = initialModelProps.resource; + } else if (isValidFullData) { + // Otherwise use the serialized id. This is only valid for local chat sessions + this._sessionId = initialData.sessionId; + this._sessionResource = LocalChatSessionUri.forSession(initialData.sessionId); + } else { + // Finally fall back to generating a new id for a local session. This is used in the case where a + // chat has been exported (but not serialized) + this._sessionId = generateUuid(); + this._sessionResource = LocalChatSessionUri.forSession(this._sessionId); + } + this._disableBackgroundKeepAlive = initialModelProps.disableBackgroundKeepAlive ?? false; this._requests = initialData ? this._deserialize(initialData) : []; diff --git a/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts index 42305065ed589..268dba2ec107c 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts @@ -16,7 +16,6 @@ export interface IStartSessionProps { readonly initialData?: ISerializedChatDataReference; readonly location: ChatAgentLocation; readonly sessionResource: URI; - readonly sessionId?: string; readonly canUseTools: boolean; readonly transferEditingSession?: IChatEditingSession; readonly disableBackgroundKeepAlive?: boolean; diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index 15ab45055b365..08fc63648986e 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -954,7 +954,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'togglePeekWidgetFocus', + id: 'toggleQuickDiffWidgetFocus', weight: KeybindingWeight.EditorContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.F2), when: isQuickDiffVisible, diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 7d207e48c414d..550329a85ef9a 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -558,13 +558,12 @@ export class ReleaseNotesManager extends Disposable { font-size: var(--vscode-font-size); font-family: var(--vscode-font-family); white-space: nowrap; - box-shadow: 1px 1px 1px rgba(0,0,0,.25); + box-shadow: 0 2px 8px var(--vscode-widget-shadow); z-index: 100; overflow: hidden; display: flex; align-items: center; justify-content: center; - transition: border-radius 0.25s ease, padding 0.25s ease, width 0.25s ease; } #update-action-btn .icon { @@ -587,16 +586,18 @@ export class ReleaseNotesManager extends Disposable { max-width: 0; opacity: 0; margin-left: 0; - transition: max-width 0.25s ease, opacity 0.2s ease, margin-left 0.25s ease; } #update-action-btn:hover, #update-action-btn.expanded { background-color: var(--vscode-button-hoverBackground); - box-shadow: 2px 2px 2px rgba(0,0,0,.25); + box-shadow: 0 2px 8px var(--vscode-widget-shadow); width: auto; - border-radius: 20px; - padding: 0 14px; + height: auto; + max-height: 40px; + border-radius: var(--vscode-cornerRadius-small); + padding: 6px 10px; + line-height: 16px; } #update-action-btn:hover .label, @@ -608,6 +609,7 @@ export class ReleaseNotesManager extends Disposable { #update-action-btn.expanded { background-color: var(--vscode-button-background); + box-shadow: 0 2px 8px var(--vscode-widget-shadow); } body.vscode-high-contrast #update-action-btn { diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index d7a24923b9e7e..d782d8df7baf0 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -20,6 +20,24 @@ import { DeepPartial } from '../../../../base/common/types.js'; export const IEditorGroupsService = createDecorator('editorGroupsService'); +export const enum GroupActivationReason { + + /** + * Group was activated explicitly by user or programmatic action. + */ + DEFAULT = 0, + + /** + * Group was activated because a modal or auxiliary editor part was closing. + */ + PART_CLOSE = 1 +} + +export interface IEditorGroupActivationEvent { + readonly group: IEditorGroup; + readonly reason: GroupActivationReason; +} + export const enum GroupDirection { UP, DOWN, @@ -212,7 +230,7 @@ export interface IEditorGroupsContainer { /** * An event for when a group gets activated. */ - readonly onDidActivateGroup: Event; + readonly onDidActivateGroup: Event; /** * An event for when the index of a group changes. diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 8c44dd65409a5..3a42f865c9630 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, ITestInstantiationService, workbenchTeardown, createEditorParts, TestEditorParts } from '../../../../test/browser/workbenchTestServices.js'; -import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement, IEditorGroupContextKeyProvider } from '../../common/editorGroupsService.js'; +import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement, IEditorGroupContextKeyProvider, GroupActivationReason, IEditorGroupActivationEvent } from '../../common/editorGroupsService.js'; import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities, GroupModelChangeKind, SideBySideEditor, IEditorFactoryRegistry, EditorExtensions } from '../../../../common/editor.js'; import { URI } from '../../../../../base/common/uri.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; @@ -2197,5 +2197,36 @@ suite('EditorGroupsService', () => { disposables.dispose(); }); + test('onDidActivateGroup carries activation reason', async function () { + const [part] = await createPart(); + + const activationEvents: IEditorGroupActivationEvent[] = []; + disposables.add(part.onDidActivateGroup(e => activationEvents.push(e))); + + const rootGroup = part.groups[0]; + const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + + // Activate a group explicitly - should carry DEFAULT reason + activationEvents.length = 0; + part.activateGroup(rightGroup); + assert.strictEqual(activationEvents.length, 1); + assert.strictEqual(activationEvents[0].group, rightGroup); + assert.strictEqual(activationEvents[0].reason, GroupActivationReason.DEFAULT); + + // Activate the same group again - should still fire with DEFAULT reason + activationEvents.length = 0; + part.activateGroup(rightGroup); + assert.strictEqual(activationEvents.length, 1); + assert.strictEqual(activationEvents[0].group, rightGroup); + assert.strictEqual(activationEvents[0].reason, GroupActivationReason.DEFAULT); + + // Activate root group back + activationEvents.length = 0; + part.activateGroup(rootGroup); + assert.strictEqual(activationEvents.length, 1); + assert.strictEqual(activationEvents[0].group, rootGroup); + assert.strictEqual(activationEvents[0].reason, GroupActivationReason.DEFAULT); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/extensions/common/extensionPoints.json b/src/vs/workbench/services/extensions/common/extensionPoints.json new file mode 100644 index 0000000000000..4d531bcf87db8 --- /dev/null +++ b/src/vs/workbench/services/extensions/common/extensionPoints.json @@ -0,0 +1,58 @@ +[ + "authentication", + "breakpoints", + "chatAgents", + "chatContext", + "chatInstructions", + "chatOutputRenderers", + "chatParticipants", + "chatPromptFiles", + "chatSessions", + "chatSkills", + "chatViewsWelcome", + "colors", + "commands", + "configuration", + "configurationDefaults", + "continueEditSession", + "css", + "customEditors", + "debugVisualizers", + "debuggers", + "grammars", + "iconThemes", + "icons", + "jsonValidation", + "keybindings", + "languageModelChatProviders", + "languageModelToolSets", + "languageModelTools", + "languages", + "localizations", + "mcpServerDefinitionProviders", + "menus", + "notebookPreload", + "notebookRenderer", + "notebooks", + "problemMatchers", + "problemPatterns", + "productIconThemes", + "remoteCodingAgents", + "remoteHelp", + "resourceLabelFormatters", + "semanticTokenModifiers", + "semanticTokenScopes", + "semanticTokenTypes", + "snippets", + "speechProviders", + "statusBarItems", + "submenus", + "taskDefinitions", + "terminal", + "terminalQuickFixes", + "themes", + "views", + "viewsContainers", + "viewsWelcome", + "walkthroughs" +] diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index ba91f4f6ac80a..4d70e2e4a4b26 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -149,7 +149,7 @@ import { CodeEditorService } from '../../services/editor/browser/codeEditorServi import { EditorPaneService } from '../../services/editor/browser/editorPaneService.js'; import { EditorResolverService } from '../../services/editor/browser/editorResolverService.js'; import { CustomEditorLabelService, ICustomEditorLabelService } from '../../services/editor/common/customEditorLabelService.js'; -import { EditorGroupLayout, GroupDirection, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, ICloseAllEditorsOptions, ICloseEditorOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorGroup, IEditorGroupContextKeyProvider, IEditorGroupsContainer, IEditorGroupsService, IEditorPart, IEditorReplacement, IEditorWorkingSet, IEditorWorkingSetOptions, IFindGroupScope, IMergeGroupOptions, IModalEditorPart } from '../../services/editor/common/editorGroupsService.js'; +import { EditorGroupLayout, GroupDirection, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, ICloseAllEditorsOptions, ICloseEditorOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorGroup, IEditorGroupActivationEvent, IEditorGroupContextKeyProvider, IEditorGroupsContainer, IEditorGroupsService, IEditorPart, IEditorReplacement, IEditorWorkingSet, IEditorWorkingSetOptions, IFindGroupScope, IMergeGroupOptions, IModalEditorPart } from '../../services/editor/common/editorGroupsService.js'; import { IEditorPaneService } from '../../services/editor/common/editorPaneService.js'; import { IEditorResolverService } from '../../services/editor/common/editorResolverService.js'; import { IEditorsChangeEvent, IEditorService, IRevertAllEditorsOptions, ISaveEditorsOptions, ISaveEditorsResult, PreferredGroup } from '../../services/editor/common/editorService.js'; @@ -863,7 +863,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { readonly onDidCreateAuxiliaryEditorPart: Event = Event.None; readonly onDidChangeActiveGroup: Event = Event.None; - readonly onDidActivateGroup: Event = Event.None; + readonly onDidActivateGroup: Event = Event.None; readonly onDidAddGroup: Event = Event.None; readonly onDidRemoveGroup: Event = Event.None; readonly onDidMoveGroup: Event = Event.None; diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index df078abc002aa..c3641a37061e9 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -102,7 +102,7 @@ declare module 'vscode' { * * This is also called on first load to get the initial set of items. */ - refreshHandler: (token: CancellationToken) => Thenable; + readonly refreshHandler: (token: CancellationToken) => Thenable; /** * Fired when an item's archived state changes. @@ -204,33 +204,33 @@ declare module 'vscode' { /** * Timestamp when the session was created in milliseconds elapsed since January 1, 1970 00:00:00 UTC. */ - created: number; + readonly created: number; /** * Timestamp when the most recent request started in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * * Should be undefined if no requests have been made yet. */ - lastRequestStarted?: number; + readonly lastRequestStarted?: number; /** * Timestamp when the most recent request completed in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * * Should be undefined if the most recent request is still in progress or if no requests have been made yet. */ - lastRequestEnded?: number; + readonly lastRequestEnded?: number; /** * Session start timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * @deprecated Use `created` and `lastRequestStarted` instead. */ - startTime?: number; + readonly startTime?: number; /** * Session end timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * @deprecated Use `lastRequestEnded` instead. */ - endTime?: number; + readonly endTime?: number; }; /** diff --git a/test/sanity/src/uiTest.ts b/test/sanity/src/uiTest.ts index cf06f01302548..06d2dc584887f 100644 --- a/test/sanity/src/uiTest.ts +++ b/test/sanity/src/uiTest.ts @@ -125,7 +125,7 @@ export class UITest { this.context.log('Typing extension name to search for'); await page.getByText('Search Extensions in Marketplace').focus(); - await page.keyboard.type('GitHub Pull Requests'); + await page.keyboard.insertText('GitHub Pull Requests'); this.context.log('Clicking Install on the first extension in the list'); await page.locator('.extension-list-item').getByText(/^GitHub Pull Requests$/).waitFor();