Skip to content

Commit

Permalink
Add option to control startup behavior per language (#6145)
Browse files Browse the repository at this point in the history
This change adds a new option that can be used to gain more control of
how Positron starts an interpreter for each language.

<img width="614" alt="image"
src="https://github.com/user-attachments/assets/cb609ebb-d170-4fda-95da-009d5cb07db4"
/>

The default value, _auto_, gives the automatic behavior we have today;
the others (enumerated in documentation elsewhere) are new behaviors.

In addition, this PR adds a new feature that allows a language pack to
tell Positron which runtime to start for a workspace _before_
discovering all the runtimes on the machine. This happens during the
_Starting_ phase of the startup lifecycle.

Addresses #3575.


### Release Notes

#### New Features

- New "startup behavior" option to control whether specific languages
will start a console in new Positron windows (#3575).

#### Bug Fixes

- N/A


### QA Notes

This new option replaces the old `interpreters.automaticStartup` option
(I think having both would be too confusing). If memory serves we're
using that today in the `qa-example-content` repro. PR updating this
option here: posit-dev/qa-example-content#44

As a debugging aid, I've added a way to show and clear which runtimes
are "affiliated" with the workspace. The new `Interpreter: Clear Saved
Interpreter` command can be used to show you which R/Python/etc.
versions Positron has saved as the ones for your workspace.

<img width="623" alt="image"
src="https://github.com/user-attachments/assets/dbd27b05-c914-4e4c-b759-13c2c45720e6"
/>

We will ultimately need more and better UI for choosing what interpreter
is associated with your project; this command should be seen as a
stopgap/debugging aid.

I've also added a couple of options to the Zed test language runtime so
that in a dev build you can try out the 'recommended for workspace'
workflow.

<img width="375" alt="image"
src="https://github.com/user-attachments/assets/5fca1868-cbb5-4ff8-a365-64f438ee333c"
/>

---------

Signed-off-by: Jonathan <[email protected]>
Co-authored-by: sharon <[email protected]>
  • Loading branch information
jmcphers and sharon-wang authored Jan 30, 2025
1 parent b672bb9 commit 5177ff7
Show file tree
Hide file tree
Showing 23 changed files with 667 additions and 151 deletions.
15 changes: 12 additions & 3 deletions extensions/positron-javascript/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
* Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

Expand Down Expand Up @@ -33,7 +33,7 @@ class JavascriptRuntimeManager implements positron.LanguageRuntimeManager {
constructor(private readonly _context: vscode.ExtensionContext) {
}

discoverRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata, any, unknown> {
discoverAllRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata, any, unknown> {
const version = process.version;

const iconSvgPath = path.join(this._context.extensionPath, 'resources', 'nodejs-icon.svg');
Expand Down Expand Up @@ -61,6 +61,15 @@ class JavascriptRuntimeManager implements positron.LanguageRuntimeManager {
}();
}

/**
* Returns the recommended runtime for the current workspace. Javascript is
* not currently detected as a recommended environment, so this always
* returns nothing.
*/
recommendedWorkspaceRuntime(): Promise<positron.LanguageRuntimeMetadata | undefined> {
return Promise.resolve(undefined);
}

createSession(
runtimeMetadata: positron.LanguageRuntimeMetadata,
sessionMetadata: positron.RuntimeSessionMetadata): Thenable<positron.LanguageRuntimeSession> {
Expand All @@ -79,7 +88,7 @@ function startExtHostRuntime(context: vscode.ExtensionContext): void {
try {
_manager = new JavascriptRuntimeManager(context);
context.subscriptions.push(
positron.runtime.registerLanguageRuntimeManager(_manager));
positron.runtime.registerLanguageRuntimeManager('javascript', _manager));
// Start the runtime on the next tick
setTimeout(() => {
positron.runtime.selectLanguageRuntime(runtimeId);
Expand Down
13 changes: 11 additions & 2 deletions extensions/positron-python/src/client/positron/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class PythonRuntimeManager implements IPythonRuntimeManager {
) {
this.onDidDiscoverRuntime = this.onDidDiscoverRuntimeEmitter.event;

positron.runtime.registerLanguageRuntimeManager(this);
positron.runtime.registerLanguageRuntimeManager('python', this);

this.disposables.push(
// When an interpreter is added, register a corresponding language runtime.
Expand Down Expand Up @@ -90,10 +90,19 @@ export class PythonRuntimeManager implements IPythonRuntimeManager {
*
* @returns An async generator that yields Python language runtime metadata.
*/
discoverRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
discoverAllRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
return this.discoverPythonRuntimes();
}

/**
* Recommend a Python language runtime based on the workspace.
*/
async recommendedWorkspaceRuntime(): Promise<positron.LanguageRuntimeMetadata | undefined> {
// TODO: This is where we could recommend a runtime based on the
// workspace, e.g. if it contains a virtualenv
return undefined;
}

/**
* An event that fires when a new Python language runtime is discovered.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ suite('Python runtime manager', () => {
});

test('constructor', () => {
verify(mockedPositronNamespaces.runtime!.registerLanguageRuntimeManager(pythonRuntimeManager)).once();
verify(mockedPositronNamespaces.runtime!.registerLanguageRuntimeManager('python', pythonRuntimeManager)).once();
});

/** Helper function to assert that a language runtime is registered. */
Expand Down
2 changes: 1 addition & 1 deletion extensions/positron-r/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(LOGGER.onDidChangeLogLevel(onDidChangeLogLevel));
onDidChangeLogLevel(LOGGER.logLevel);

positron.runtime.registerLanguageRuntimeManager(new RRuntimeManager(context));
positron.runtime.registerLanguageRuntimeManager('r', new RRuntimeManager(context));

// Set contexts.
setContexts(context);
Expand Down
8 changes: 7 additions & 1 deletion extensions/positron-r/src/runtime-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ export class RRuntimeManager implements positron.LanguageRuntimeManager {

constructor(private readonly _context: vscode.ExtensionContext) { }

discoverRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
discoverAllRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
return rRuntimeDiscoverer();
}

async recommendedWorkspaceRuntime(): Promise<positron.LanguageRuntimeMetadata | undefined> {
// TODO: If the workspace contains an R project, we could recommend an
// R runtime from e.g. the `DESCRIPTION` file or an renv lockfile.
return undefined;
}

createSession(
runtimeMetadata: positron.LanguageRuntimeMetadata,
sessionMetadata: positron.RuntimeSessionMetadata): Thenable<positron.LanguageRuntimeSession> {
Expand Down
8 changes: 6 additions & 2 deletions extensions/positron-reticulate/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,16 @@ export class ReticulateRuntimeManager implements positron.LanguageRuntimeManager
}
}

discoverRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
discoverAllRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
// We never discover a runtime directly. We'll always check if R is available
// and then fire the onDidDiscoverRuntime event.
return (async function* () { })();
}

async recommendedWorkspaceRuntime(): Promise<positron.LanguageRuntimeMetadata | undefined> {
return undefined;
}

async createSession(runtimeMetadata: positron.LanguageRuntimeMetadata, sessionMetadata: positron.RuntimeSessionMetadata): Promise<positron.LanguageRuntimeSession> {
try {
this._session = await ReticulateRuntimeSession.create(runtimeMetadata, sessionMetadata);
Expand Down Expand Up @@ -722,7 +726,7 @@ export class ReticulateProvider {

constructor(readonly context: vscode.ExtensionContext) {
this.manager = new ReticulateRuntimeManager(this.context);
this.context.subscriptions.push(positron.runtime.registerLanguageRuntimeManager(this.manager));
this.context.subscriptions.push(positron.runtime.registerLanguageRuntimeManager('python', this.manager));
}

async registerClient(client: positron.RuntimeClientInstance) {
Expand Down
28 changes: 27 additions & 1 deletion extensions/positron-zed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,33 @@
"application/vnd.zed.clock"
]
}
]
],
"configuration": {
"type": "object",
"title": "Zed Language",
"properties": {
"zedLanguage.preferredZed": {
"type": "string",
"default": "none",
"description": "The preferred version of Zed to use",
"enum": [
"none",
"1.0.0",
"2.0.0"
],
"enumDescriptions": [
"No version is preferred",
"Zed 1.0.0",
"Zed 2.0.0"
]
},
"zedLanguage.autoStartup": {
"type": "boolean",
"default": false,
"description": "Automatically start Zed in new workspaces"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
Expand Down
2 changes: 1 addition & 1 deletion extensions/positron-zed/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ import { ZedRuntimeManager } from './manager';
*/
export function activate(context: vscode.ExtensionContext) {
// Register the Zed runtime manager with the Positron runtime.
positron.runtime.registerLanguageRuntimeManager(new ZedRuntimeManager(context));
positron.runtime.registerLanguageRuntimeManager('zed', new ZedRuntimeManager(context));
}
40 changes: 34 additions & 6 deletions extensions/positron-zed/src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import { PositronZedRuntimeSession } from './positronZedLanguageRuntime';
* @param context The VS Code extension context.
* @param runtimeId The ID to assign to the runtime.
* @param version The version of the runtime.
* @param startupBehavior The startup behavior for the runtime.
*
* @returns A full runtime metadata object.
*/
function generateZedMetadata(context: vscode.ExtensionContext,
runtimeId: string,
version: string): positron.LanguageRuntimeMetadata {
version: string,
startupBehavior: positron.LanguageRuntimeStartupBehavior): positron.LanguageRuntimeMetadata {

// Create the icon SVG path.
const iconSvgPath = path.join(context.extensionPath, 'resources', 'zed-icon.svg');
Expand All @@ -41,7 +43,7 @@ function generateZedMetadata(context: vscode.ExtensionContext,
base64EncodedIconSvg: fs.readFileSync(iconSvgPath).toString('base64'),
runtimeVersion: '0.0.1',
sessionLocation: positron.LanguageRuntimeSessionLocation.Browser,
startupBehavior: positron.LanguageRuntimeStartupBehavior.Implicit,
startupBehavior,
extraRuntimeData: {}
};

Expand All @@ -62,27 +64,53 @@ export class ZedRuntimeManager implements positron.LanguageRuntimeManager {
*
* @returns An async generator that yields metadata for the Zed language
*/
discoverRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata, any, unknown> {
discoverAllRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata, any, unknown> {
const context = this._context;

const generator = async function* getPositronZedLanguageRuntimes() {
yield generateZedMetadata(
context,
'00000000-0000-0000-0000-000000000200',
'2.0.0');
'2.0.0',
positron.LanguageRuntimeStartupBehavior.Implicit);
yield generateZedMetadata(
context,
'00000000-0000-0000-0000-000000000100',
'1.0.0');
'1.0.0',
positron.LanguageRuntimeStartupBehavior.Implicit);
yield generateZedMetadata(
context,
'00000000-0000-0000-0000-000000000098',
'0.98.0');
'0.98.0',
positron.LanguageRuntimeStartupBehavior.Implicit);
};

return generator();
}

/**
* Recommends a Zed runtime for the workspace.
*/
async recommendedWorkspaceRuntime(): Promise<positron.LanguageRuntimeMetadata | undefined> {
// See if the user has a preferred Zed runtime.
const preferredZed = vscode.workspace.getConfiguration('zedLanguage').get<string>('preferredZed');
if (!preferredZed || preferredZed === 'none') {
return undefined;
}

// Determine the startup behavior.
const autoStart = vscode.workspace.getConfiguration('zedLanguage').get<boolean>('autoStartup');

// Create the metadata for the preferred Zed runtime.
return generateZedMetadata(
this._context,
preferredZed === '2.0.0' ? '00000000-0000-0000-0000-000000000200' :
preferredZed === '1.0.0' ? '00000000-0000-0000-0000-000000000100' :
'00000000-0000-0000-0000-000000000098',
preferredZed,
autoStart ? positron.LanguageRuntimeStartupBehavior.Immediate : positron.LanguageRuntimeStartupBehavior.Explicit);
}

/**
* Start a new session for the Zed language runtime.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,14 @@ class TestLanguageRuntimeManager implements positron.LanguageRuntimeManager {

onDidDiscoverRuntime = this.onDidDiscoverRuntimeEmitter.event;

async* discoverRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
async* discoverAllRuntimes(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
yield testLanguageRuntimeMetadata();
}

async recommendedWorkspaceRuntime(): Promise<positron.LanguageRuntimeMetadata | undefined> {
return undefined;
}

async createSession(
runtimeMetadata: positron.LanguageRuntimeMetadata,
sessionMetadata: positron.RuntimeSessionMetadata
Expand Down Expand Up @@ -145,7 +149,7 @@ suite('positron API - runtime', () => {

// Register a manager.
const manager = new TestLanguageRuntimeManager();
const managerDisposable = positron.runtime.registerLanguageRuntimeManager(manager);
const managerDisposable = positron.runtime.registerLanguageRuntimeManager('test', manager);

// The manager's runtimes should eventually be registered.
await poll(
Expand Down
31 changes: 24 additions & 7 deletions src/positron-dts/positron.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
* Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

Expand Down Expand Up @@ -648,13 +648,27 @@ declare module 'positron' {

export interface LanguageRuntimeManager {
/**
* Returns a generator that yields metadata about the language runtimes
* that are available to the user.
* Returns a generator that yields metadata about all the language
* runtimes that are available to the user.
*
* This metadata will be passed to `createSession` to create new runtime
* sessions.
*/
discoverRuntimes(): AsyncGenerator<LanguageRuntimeMetadata>;
discoverAllRuntimes(): AsyncGenerator<LanguageRuntimeMetadata>;

/**
* Returns a single runtime metadata object representing the runtime
* that should be used in the current workspace, if any.
*
* Note that this is called before `discoverAllRuntimes` during
* startup, and should return `undefined` if no runtime is recommended.
*
* If a runtime is returned, `startupBehavior` property of the runtime
* metadata is respected here; use `Immediately` to start the runtime
* right away, or any other value to save the runtime as the project
* default without starting it.
*/
recommendedWorkspaceRuntime(): Thenable<LanguageRuntimeMetadata | undefined>;

/**
* An optional event that fires when a new runtime is discovered.
Expand Down Expand Up @@ -1354,10 +1368,13 @@ declare module 'positron' {
errorBehavior?: RuntimeErrorBehavior): Thenable<boolean>;

/**
* Register a language runtime manager with Positron. Returns a
* disposable that unregisters the manager when disposed.
* Register a language runtime manager with Positron.
*
* @param languageId The language ID for which the runtime
* @returns A disposable that unregisters the manager when disposed.
*
*/
export function registerLanguageRuntimeManager(manager: LanguageRuntimeManager): vscode.Disposable;
export function registerLanguageRuntimeManager(languageId: string, manager: LanguageRuntimeManager): vscode.Disposable;

/**
* List all registered runtimes.
Expand Down
Loading

0 comments on commit 5177ff7

Please sign in to comment.