diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json index 4b7a096bd04..e0d0b79ecdb 100644 --- a/extensions/positron-r/package.json +++ b/extensions/positron-r/package.json @@ -671,6 +671,9 @@ "which": "^3.0.0", "xdg-portable": "^10.6.0" }, + "extensionDependencies": [ + "positron.positron-supervisor" + ], "peerDependencies": { "@vscode/windows-registry": "^1.0.0" }, diff --git a/extensions/python/package.json b/extensions/python/package.json index a1cae47db17..c10b1c02086 100644 --- a/extensions/python/package.json +++ b/extensions/python/package.json @@ -57,6 +57,9 @@ "scripts": { "update-grammar": "node ../node_modules/vscode-grammar-updater/bin MagicStack/MagicPython grammars/MagicPython.tmLanguage ./syntaxes/MagicPython.tmLanguage.json grammars/MagicRegExp.tmLanguage ./syntaxes/MagicRegExp.tmLanguage.json" }, + "extensionDependencies": [ + "positron.positron-supervisor" + ], "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/runtimeStartupProgress.css b/src/vs/workbench/contrib/positronConsole/browser/components/runtimeStartupProgress.css new file mode 100644 index 00000000000..5ade4cddf52 --- /dev/null +++ b/src/vs/workbench/contrib/positronConsole/browser/components/runtimeStartupProgress.css @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +.runtime-starting { + display: flex; + flex-direction: column; +} + +.runtime-starting .action, +.runtime-starting .runtime-name { + margin: 2px; + text-transform: uppercase; + text-align: center; +} + +.runtime-starting .action { + font-size: 11px; +} + +.runtime-starting .runtime-name { + font-weight: bold; +} + +.runtime-starting-icon { + height: 50px; +} diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/runtimeStartupProgress.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/runtimeStartupProgress.tsx new file mode 100644 index 00000000000..a6b2c0c898d --- /dev/null +++ b/src/vs/workbench/contrib/positronConsole/browser/components/runtimeStartupProgress.tsx @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +// CSS. +import './runtimeStartupProgress.css'; + +// React. +import React from 'react'; + +// Other dependencies. +import { localize } from '../../../../../nls.js'; +import { IRuntimeAutoStartEvent } from '../../../../services/runtimeStartup/common/runtimeStartupService.js'; + +// RuntimeStartupProgressProps interface. +export interface RuntimeStartupProgressProps { + evt: IRuntimeAutoStartEvent; +} + +const preparing = localize('positron.runtimeStartup.newSession', "Preparing"); +const reconnecting = localize('positron.runtimeStartup.existingSession', "Reconnecting"); + +/** + * RuntimeStartupProgress component. + * + * This component renders the status for a runtime that is about to start up. + * It's only rendered before any runtime actually starts in new Positron + * windows. + * + * @param props A RuntimeStartupProgressProps that contains the component + * properties. + * @returns The rendered component. + */ +export const RuntimeStartupProgress = (props: RuntimeStartupProgressProps) => { + // Render. + return ( +
+ +
{props.evt.runtime.runtimeName}
+
{props.evt.newSession ? preparing : reconnecting}
+
+ ); +}; + diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx index c77a1799193..f434425e18d 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx @@ -15,6 +15,8 @@ import { usePositronConsoleContext } from '../positronConsoleContext.js'; import { ProgressBar } from '../../../../../base/browser/ui/progressbar/progressbar.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { RuntimeStartupPhase } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; +import { IRuntimeAutoStartEvent } from '../../../../services/runtimeStartup/common/runtimeStartupService.js'; +import { RuntimeStartupProgress } from './runtimeStartupProgress.js'; // Load localized copy for control. const initalizing = localize('positron.console.initializing', "Starting up"); @@ -42,6 +44,8 @@ export const StartupStatus = () => { useState(positronConsoleContext.languageRuntimeService.registeredRuntimes.length); const [startupPhase, setStartupPhase] = useState(positronConsoleContext.languageRuntimeService.startupPhase); + const [runtimeStartupEvent, setRuntimeStartupEvent] = + useState(undefined); useEffect(() => { const disposableStore = new DisposableStore(); @@ -70,6 +74,15 @@ export const StartupStatus = () => { setStartupPhase(phase); })); + // When we're notified that a runtime may auto-start in the workspace, + // show it. Note that this event is not reliable as a signal that a + // runtime will actually start; see notes in the RuntimeStartupService. + disposableStore.add( + positronConsoleContext.runtimeStartupService.onWillAutoStartRuntime( + evt => { + setRuntimeStartupEvent(evt); + })); + // Return the cleanup function that will dispose of the disposables. return () => { bar?.done(); @@ -81,19 +94,22 @@ export const StartupStatus = () => { return (
+ {runtimeStartupEvent && + + } {startupPhase === RuntimeStartupPhase.Initializing &&
{initalizing}...
} - {startupPhase === RuntimeStartupPhase.Reconnecting && -
{reconnecting}...
+ {startupPhase === RuntimeStartupPhase.Reconnecting && !runtimeStartupEvent && +
{reconnecting}...
} {startupPhase === RuntimeStartupPhase.AwaitingTrust &&
{awaitingTrust}...
} - {startupPhase === RuntimeStartupPhase.Starting && + {startupPhase === RuntimeStartupPhase.Starting && !runtimeStartupEvent &&
{starting}...
} - {startupPhase === RuntimeStartupPhase.Discovering && + {startupPhase === RuntimeStartupPhase.Discovering && !runtimeStartupEvent &&
{discoveringIntrepreters} {discovered > 0 && ({discovered})}...
} diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index da6ee353dce..f7e356519e8 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -1015,18 +1015,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return this._installedExtensionsReady.wait(); } - // --- Start Positron --- - /** - * Wait for all extension hosts to start, and for all eagerly activated - * extensions to activate. - * - * @returns A promise that resolves when all extension hosts have started. - */ - public whenAllExtensionHostsStarted(): Promise { - return this._allExtensionHostsStarted.wait(); - } - // --- End Positron --- - get extensions(): IExtensionDescription[] { return this._registry.getAllExtensionDescriptions(); } diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index d798d92a046..2d81c4f35bf 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -480,14 +480,6 @@ export interface IExtensionService { */ whenInstalledExtensionsRegistered(): Promise; - // --- Start Positron --- - /** - * An promise that resolves when all the extension hosts have started and - * eager extensions have been activated. - */ - whenAllExtensionHostsStarted(): Promise; - // --- End Positron --- - /** * Return a specific extension * @param id An extension id @@ -603,9 +595,6 @@ export class NullExtensionService implements IExtensionService { activateById(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { return Promise.resolve(undefined); } activationEventIsDone(_activationEvent: string): boolean { return false; } whenInstalledExtensionsRegistered(): Promise { return Promise.resolve(true); } - // --- Start Positron --- - whenAllExtensionHostsStarted(): Promise { return Promise.resolve(true); } - // --- End Positron --- getExtension() { return Promise.resolve(undefined); } readExtensionPointContributions(_extPoint: IExtensionPoint): Promise[]> { return Promise.resolve(Object.create(null)); } getExtensionsStatus(): { [id: string]: IExtensionsStatus } { return Object.create(null); } diff --git a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts index 1e6c20ee2df..f0ae1d789b0 100644 --- a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts +++ b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts @@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { IEphemeralStateService } from '../../../../platform/ephemeralState/common/ephemeralState.js'; import { IExtensionService } from '../../extensions/common/extensions.js'; import { ILanguageRuntimeExit, ILanguageRuntimeMetadata, ILanguageRuntimeService, IRuntimeManager, LanguageRuntimeSessionLocation, LanguageRuntimeSessionMode, LanguageRuntimeStartupBehavior, RuntimeExitReason, RuntimeStartupPhase, RuntimeState, LanguageStartupBehavior, formatLanguageRuntimeMetadata } from '../../languageRuntime/common/languageRuntimeService.js'; -import { IRuntimeStartupService } from './runtimeStartupService.js'; +import { IRuntimeAutoStartEvent, IRuntimeStartupService } from './runtimeStartupService.js'; import { ILanguageRuntimeSession, IRuntimeSessionMetadata, IRuntimeSessionService, RuntimeStartMode } from '../../runtimeSession/common/runtimeSessionService.js'; import { ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; @@ -24,6 +24,7 @@ import { URI } from '../../../../base/common/uri.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { IPositronNewProjectService } from '../../positronNewProject/common/positronNewProject.js'; import { isWeb } from '../../../../base/common/platform.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; interface ILanguageRuntimeProviderMetadata { languageId: string; @@ -115,6 +116,9 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup /// running one or extensions that provide runtimes. private _runtimeManagers: IRuntimeManager[] = []; + /// The event emitter for the onWillAutoStartRuntime event. + private readonly _onWillAutoStartRuntime: Emitter; + constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IExtensionService private readonly _extensionService: IExtensionService, @@ -132,6 +136,10 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup super(); + this._onWillAutoStartRuntime = new Emitter(); + this._register(this._onWillAutoStartRuntime); + this.onWillAutoStartRuntime = this._onWillAutoStartRuntime.event; + this._register( this._runtimeSessionService.onDidChangeForegroundSession( this.onDidChangeActiveRuntime, this)); @@ -231,7 +239,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Start the first runtime that has Immediate startup behavior if (languageRuntimes.length) { const extension = languageRuntimes[0].extensionId; - this._runtimeSessionService.autoStartRuntime(languageRuntimes[0], + this.autoStartRuntime(languageRuntimes[0], `The ${extension.value} extension requested the runtime to be started immediately.`, true); return; @@ -262,7 +270,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup return always; }); if (alwaysStarted.length) { - this._runtimeSessionService.autoStartRuntime(alwaysStarted[0], + this.autoStartRuntime(alwaysStarted[0], `The configuration specifies that a runtime should always start for the '${languageId}' language.`, true); } @@ -287,7 +295,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup this._startupPhase === RuntimeStartupPhase.Complete && !this._runtimeSessionService.hasStartingOrRunningConsole()) { - this._runtimeSessionService.autoStartRuntime(runtime, + this.autoStartRuntime(runtime, `An extension requested that the runtime start immediately after being registered.`, true); } @@ -305,36 +313,12 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup runtime.startupBehavior === LanguageRuntimeStartupBehavior.Implicit && !this.getAffiliatedRuntimeMetadata(runtime.languageId)) { - this._runtimeSessionService.autoStartRuntime(runtime, + this.autoStartRuntime(runtime, `A file with the language ID ${runtime.languageId} was open ` + `when the runtime was registered.`, true); } })); - // Wait for all extension hosts to start before beginning the main - // startup sequence. - this._extensionService.whenAllExtensionHostsStarted().then(async () => { - if (this._workspaceTrustManagementService.isWorkspaceTrusted()) { - // In a trusted workspace, we can start the startup sequence - // immediately. - await this.startupSequence(); - } else { - // If we are not in a trusted workspace, wait for the workspace to become - // trusted before starting the startup sequence. - this.setStartupPhase(RuntimeStartupPhase.AwaitingTrust); - this._register(this._workspaceTrustManagementService.onDidChangeTrust((trusted) => { - if (!trusted) { - return; - } - // If the workspace becomse trusted while we are awaiting trust, - // move on to the startup sequence. - if (this._startupPhase === RuntimeStartupPhase.AwaitingTrust) { - this.startupSequence(); - } - })); - } - }); - this._register(languageRuntimeExtPoint.setHandler((extensions) => { // This new set of extensions replaces the old set, so clear the // language packs. @@ -362,6 +346,10 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup this._logService.debug(`[Runtime startup] No language packs were found.`); this.setStartupPhase(RuntimeStartupPhase.Complete); } + } else if (this._startupPhase === RuntimeStartupPhase.Initializing && this._languagePacks.size > 0) { + // If we just got language packs, and we were in the Initializing + // phase, move on to the startup phase. + this.startupAfterTrust(); } })); @@ -393,6 +381,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup })); } + onWillAutoStartRuntime: Event; /** * Gets all the affiliated runtimes for the workspace. @@ -642,9 +631,6 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup return; } - // Ensure all extension hosts are started before we start activating extensions - await this._extensionService.whenAllExtensionHostsStarted(); - // Filter out any language packs that are disabled. const disabledLanguages = new Array(); const enabledLanguages = Array.from(this._languagePacks.keys()).filter(languageId => { @@ -891,7 +877,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup if (runtime.startupBehavior === LanguageRuntimeStartupBehavior.Immediate) { // Start the runtime immediately if it has Immediate startup // behavior. - this._runtimeSessionService.autoStartRuntime(runtime, + this.autoStartRuntime(runtime, `The ${runtime.extensionId.value} extension recommended the runtime to be started in this workspace.`, idx === 0); } else { @@ -937,10 +923,6 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup return; } - // Activate all the extensions that provide language runtimes for the - // affiliated languages. - await this.activateExtensionsForLanguages(languageIds); - // Start the affiliated runtimes. languageIds.map(languageId => { // Get the affiliated runtime metadata. @@ -984,7 +966,18 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Sort the affiliations by last used time, so that the most recently // used runtime is started first return b.lastUsed - a.lastUsed; - }).map((affiliation, idx) => { + }).map(async (affiliation, idx) => { + if (idx === 0) { + // Let the UI know we're about to try starting this session + this._onWillAutoStartRuntime.fire({ + runtime: affiliation.metadata, + newSession: true + }); + } + + // Activate the associated extension + await this.activateExtensionsForLanguages([affiliation.metadata.languageId]); + // Start each runtime. Activate the first one as soon as it's // ready; let the others start in the background. this.startAffiliatedRuntime(affiliation, idx === 0); @@ -1073,7 +1066,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup affiliatedRuntime.lastStarted = Date.now(); this.saveAffiliatedRuntime(affiliatedRuntime); - this._runtimeSessionService.autoStartRuntime(affiliatedRuntimeMetadata, + this.autoStartRuntime(affiliatedRuntimeMetadata, `Affiliated ${affiliatedRuntimeMetadata.languageName} runtime for workspace`, activate); } @@ -1144,6 +1137,17 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup this.setStartupPhase(RuntimeStartupPhase.Reconnecting); + // Sort the sessions by last used time, so that we reconnect to the + // most recently used sessions first. Default 0 so we can restore + // sessions that didn't persist this information. + sessions.sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0)); + + // Let the UI know we're about to try reconnecting to this session + this._onWillAutoStartRuntime.fire({ + runtime: sessions[0].runtimeMetadata, + newSession: false + }); + // Activate any extensions needed for the sessions that are persistent on the machine. const activatedExtensions: Array = []; await Promise.all(sessions.filter(async session => @@ -1203,15 +1207,14 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Remove all the sessions that are no longer valid. sessions = sessions.filter((_, i) => validSessions[i]); - // Sort the sessions by last used time, so that we reconnect to the - // most recently used sessions first. Default 0 so we can restore - // sessions that didn't persist this information. - sessions.sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0)); - // Reconnect to the remaining sessions. this._logService.debug(`Reconnecting to sessions: ` + sessions.map(session => session.metadata.sessionName).join(', ')); + // Keep track of whether we are expecting to see the first console + // session + let firstConsole = true; + await Promise.all(sessions.map(async (session, idx) => { const marker = `[Reconnect ${session.metadata.sessionId} (${idx + 1}/${sessions.length})]`; @@ -1230,9 +1233,17 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup this._logService.debug(`${marker}: Restoring session for ${session.metadata.sessionName}`); - // Reconnect to the session; activate it if it is the first session + // We want to activate the first console session we see, but no + // following sessions + const activate = firstConsole; + if (!session.metadata.notebookUri) { + firstConsole = false; + } + + // Reconnect to the session; activate it if it is the first console + // session await this._runtimeSessionService.restoreRuntimeSession( - session.runtimeMetadata, session.metadata, idx === 0); + session.runtimeMetadata, session.metadata, activate); })); } @@ -1380,6 +1391,48 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup return this._configurationService.getValue( 'interpreters.startupBehavior', { overrideIdentifier: languageId }); } + + + /** + * Fires the main startup sequence, possibly after waiting for the + * workspace to be trusted. + */ + private async startupAfterTrust(): Promise { + if (this._workspaceTrustManagementService.isWorkspaceTrusted()) { + // In a trusted workspace, we can start the startup sequence + // immediately. + await this.startupSequence(); + } else { + // If we are not in a trusted workspace, wait for the workspace to become + // trusted before starting the startup sequence. + this.setStartupPhase(RuntimeStartupPhase.AwaitingTrust); + this._register(this._workspaceTrustManagementService.onDidChangeTrust((trusted) => { + if (!trusted) { + return; + } + // If the workspace becomse trusted while we are awaiting trust, + // move on to the startup sequence. + if (this._startupPhase === RuntimeStartupPhase.AwaitingTrust) { + this.startupSequence(); + } + })); + } + } + + /** + * Starts a new runtime session from implicit state. + */ + private async autoStartRuntime( + metadata: ILanguageRuntimeMetadata, + source: string, + activate: boolean + ) { + this._onWillAutoStartRuntime.fire({ + runtime: metadata, + newSession: true + }); + this._runtimeSessionService.autoStartRuntime(metadata, source, activate); + } } registerSingleton(IRuntimeStartupService, RuntimeStartupService, InstantiationType.Eager); diff --git a/src/vs/workbench/services/runtimeStartup/common/runtimeStartupService.ts b/src/vs/workbench/services/runtimeStartup/common/runtimeStartupService.ts index bda1a07f760..665cc5135c6 100644 --- a/src/vs/workbench/services/runtimeStartup/common/runtimeStartupService.ts +++ b/src/vs/workbench/services/runtimeStartup/common/runtimeStartupService.ts @@ -6,10 +6,20 @@ import { IDisposable } from '../../../../base/common/lifecycle.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILanguageRuntimeMetadata, IRuntimeManager } from '../../languageRuntime/common/languageRuntimeService.js'; +import { Event } from '../../../../base/common/event.js'; export const IRuntimeStartupService = createDecorator('runtimeStartupService'); + +/** + * An event that is emitted when a runtime is automatically started. + */ +export interface IRuntimeAutoStartEvent { + runtime: ILanguageRuntimeMetadata; + newSession: boolean; +} + /** * The IRuntimeStartupService is responsible for coordinating the process by * which runtimes are automatically started when a workspace is opened, and @@ -53,6 +63,19 @@ export interface IRuntimeStartupService { */ clearAffiliatedRuntime(languageId: string): void; + /** + * An event that is emitted when a runtime about to be automatically + * started or resumed in a new Positron window. + * + * This event is intended to help communicate startup information to the + * UI; it is not reliable as a signal that a runtime will actually start. + * It may fire for runtimes that ultimately do not start (due to e.g. stale + * metadata), and may fire multiple times for the same runtime. + * + * Use `onWillStartSession` for a reliable start signal. + */ + onWillAutoStartRuntime: Event; + /** * Signal that discovery of language runtimes is completed for an extension host. *