diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 2c957a9796f6ac..89b3416aac0263 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -340,9 +340,13 @@ export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWor // Restart extension host if first root folder changed (impact on deprecated workspace.rootPath API) const newFirstFolderResource = workspace.folders.length > 0 ? workspace.folders[0].uri : undefined; if (!isEqual(this.firstFolderResource, newFirstFolderResource)) { + const hadFirstFolder = !!this.firstFolderResource; this.firstFolderResource = newFirstFolderResource; - this.extensionHostRestarter.schedule(); // buffer calls to extension host restart + // Skip 0→1 folders (#319944): same as adding a folder in an existing workspace + if (hadFirstFolder) { + this.extensionHostRestarter.schedule(); // buffer calls to extension host restart + } } } } diff --git a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts index 1ed42eca3ec1dd..44194870922fc6 100644 --- a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts @@ -6,7 +6,7 @@ import { localize } from '../../../../nls.js'; import { IWorkspaceEditingService } from '../common/workspaceEditing.js'; import { URI } from '../../../../base/common/uri.js'; -import { hasWorkspaceFileExtension, isUntitledWorkspace, isWorkspaceIdentifier, IWorkspaceContextService, toWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js'; +import { hasWorkspaceFileExtension, isUntitledWorkspace, isWorkspaceIdentifier, IWorkspaceContextService, toWorkspaceIdentifier, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { IJSONEditingService } from '../../configuration/common/jsonEditing.js'; import { IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js'; import { WorkspaceService } from '../../configuration/browser/configurationService.js'; @@ -176,9 +176,14 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi } async enterWorkspace(workspaceUri: URI): Promise { - const stopped = await this.extensionService.stopExtensionHosts(localize('restartExtensionHost.reason', "Opening a multi-root workspace")); - if (!stopped) { - return; + // Skip extension host restart when coming from an empty window (#319944) + const fromEmptyWorkspace = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; + + if (!fromEmptyWorkspace) { + const stopped = await this.extensionService.stopExtensionHosts(localize('restartExtensionHost.reason', "Opening a multi-root workspace")); + if (!stopped) { + return; + } } const oldWorkspace = toWorkspaceIdentifier(this.contextService.getWorkspace()); @@ -205,7 +210,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi // Restart the extension host: entering a workspace means a new location for // storage and potentially a change in the workspace.rootPath property. - else { + else if (!fromEmptyWorkspace) { this.extensionService.startExtensionHosts(); } } diff --git a/src/vs/workbench/services/workspaces/test/electron-browser/nativeWorkspaceEditingService.test.ts b/src/vs/workbench/services/workspaces/test/electron-browser/nativeWorkspaceEditingService.test.ts new file mode 100644 index 00000000000000..3284633ff63a77 --- /dev/null +++ b/src/vs/workbench/services/workspaces/test/electron-browser/nativeWorkspaceEditingService.test.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { IWorkspaceContextService, toWorkspaceFolder, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; +import { Workspace } from '../../../../../platform/workspace/test/common/testWorkspace.js'; +import { IJSONEditingService } from '../../../configuration/common/jsonEditing.js'; +import { IExtensionService } from '../../../extensions/common/extensions.js'; +import { NativeWorkspaceEditingService } from '../../electron-browser/workspaceEditingService.js'; +import { TestContextService } from '../../../../test/common/workbenchTestServices.js'; +import { workbenchInstantiationService } from '../../../../test/electron-browser/workbenchTestServices.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; + +class TestNativeWorkspaceEditingService extends NativeWorkspaceEditingService { + protected override async doEnterWorkspace(workspaceUri: URI) { + return { workspace: { id: 'test-workspace', configPath: workspaceUri } }; + } +} + +suite('NativeWorkspaceEditingService', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + const workspaceUri = URI.file('/test/Untitled-1.code-workspace'); + + setup(() => { + disposables.add(instantiationService = workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IJSONEditingService, new class extends mock() { }); + }); + + function createExtensionService(): { stopCalls: number; startCalls: number } { + const calls = { stopCalls: 0, startCalls: 0 }; + instantiationService.stub(IExtensionService, new class extends mock() { + override stopExtensionHosts() { + calls.stopCalls++; + return Promise.resolve(true); + } + override startExtensionHosts() { + calls.startCalls++; + return Promise.resolve(); + } + }); + return calls; + } + + test('enterWorkspace from empty window does not restart extension host', async () => { + const contextService = new TestContextService(new Workspace('empty-workspace')); + assert.strictEqual(contextService.getWorkbenchState(), WorkbenchState.EMPTY); + instantiationService.stub(IWorkspaceContextService, contextService); + + const extensionService = createExtensionService(); + const service = disposables.add(instantiationService.createInstance(TestNativeWorkspaceEditingService)); + await service.enterWorkspace(workspaceUri); + + assert.strictEqual(extensionService.stopCalls, 0); + assert.strictEqual(extensionService.startCalls, 0); + }); + + test('enterWorkspace from folder window restarts extension host', async () => { + const contextService = new TestContextService(new Workspace('folder-workspace', [toWorkspaceFolder(URI.file('/test/folder'))])); + assert.strictEqual(contextService.getWorkbenchState(), WorkbenchState.FOLDER); + instantiationService.stub(IWorkspaceContextService, contextService); + + const extensionService = createExtensionService(); + const service = disposables.add(instantiationService.createInstance(TestNativeWorkspaceEditingService)); + await service.enterWorkspace(workspaceUri); + + assert.strictEqual(extensionService.stopCalls, 1); + assert.strictEqual(extensionService.startCalls, 1); + }); +});