Skip to content
2 changes: 2 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
"order": 4
}
},
// --- Start Positron
{
// Note this uses the version of Code/Positron you launch it from.
// i.e., launch from dev Positron if your tests need dev Positron.
Expand All @@ -205,6 +206,7 @@
"order": 5
}
},
// --- End Positron
{
"type": "extensionHost",
"request": "launch",
Expand Down
19 changes: 19 additions & 0 deletions extensions/positron-r/.zed/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"languages": {
"TypeScript": {
"tab_size": 2,
"hard_tabs": true,
"ensure_final_newline_on_save": true,
"remove_trailing_whitespace_on_save": true,
"format_on_save": "on",
"formatter": "language_server",
"code_actions_on_format": {
"source.fixAll.eslint": false
}
}
}
}
4 changes: 2 additions & 2 deletions extensions/positron-r/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export async function registerCommands(context: vscode.ExtensionContext, runtime
if (!isInstalled) {
return;
}
const session = RSessionManager.instance.getConsoleSession();
const session = await RSessionManager.instance.getConsoleSession();
if (!session) {
return;
}
Expand Down Expand Up @@ -169,7 +169,7 @@ export async function registerCommands(context: vscode.ExtensionContext, runtime
}),

vscode.commands.registerCommand('r.scriptPath', async () => {
const session = RSessionManager.instance.getConsoleSession();
const session = await RSessionManager.instance.getConsoleSession();
if (!session) {
throw new Error(`Cannot get Rscript path; no R session available`);
}
Expand Down
8 changes: 4 additions & 4 deletions extensions/positron-r/src/llm-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function registerRLanguageModelTools(context: vscode.ExtensionContext): v
const rListPackageHelpTopicsTool = vscode.lm.registerTool<{ sessionIdentifier: string; packageName: string }>('listPackageHelpTopics', {
invoke: async (options, token) => {
const manager = RSessionManager.instance;
const session = manager.getSessionById(options.input.sessionIdentifier);
const session = await manager.getSessionById(options.input.sessionIdentifier);
if (!session) {
return new vscode.LanguageModelToolResult([
new vscode.LanguageModelTextPart(`No active R session with identifier ${options.input.sessionIdentifier}`),
Expand Down Expand Up @@ -44,7 +44,7 @@ export function registerRLanguageModelTools(context: vscode.ExtensionContext): v
const rListAvailableVignettesTool = vscode.lm.registerTool<{ sessionIdentifier: string; packageName: string }>('listAvailableVignettes', {
invoke: async (options, token) => {
const manager = RSessionManager.instance;
const session = manager.getSessionById(options.input.sessionIdentifier);
const session = await manager.getSessionById(options.input.sessionIdentifier);
if (!session) {
return new vscode.LanguageModelToolResult([
new vscode.LanguageModelTextPart(`No active R session with identifier ${options.input.sessionIdentifier}`),
Expand Down Expand Up @@ -73,7 +73,7 @@ export function registerRLanguageModelTools(context: vscode.ExtensionContext): v
const rGetPackageVignetteTool = vscode.lm.registerTool<{ sessionIdentifier: string; packageName: string; vignetteName: string }>('getPackageVignette', {
invoke: async (options, token) => {
const manager = RSessionManager.instance;
const session = manager.getSessionById(options.input.sessionIdentifier);
const session = await manager.getSessionById(options.input.sessionIdentifier);
if (!session) {
return new vscode.LanguageModelToolResult([
new vscode.LanguageModelTextPart(`No active R session with identifier ${options.input.sessionIdentifier}`),
Expand Down Expand Up @@ -103,7 +103,7 @@ export function registerRLanguageModelTools(context: vscode.ExtensionContext): v
const rGetHelpPageTool = vscode.lm.registerTool<{ sessionIdentifier: string; packageName?: string; helpTopic: string }>('getHelpPage', {
invoke: async (options, token) => {
const manager = RSessionManager.instance;
const session = manager.getSessionById(options.input.sessionIdentifier);
const session = await manager.getSessionById(options.input.sessionIdentifier);
if (!session) {
return new vscode.LanguageModelToolResult([
new vscode.LanguageModelTextPart(`No active R session with identifier ${options.input.sessionIdentifier}`),
Expand Down
58 changes: 0 additions & 58 deletions extensions/positron-r/src/lsp-output-channel-manager.ts

This file was deleted.

33 changes: 19 additions & 14 deletions extensions/positron-r/src/lsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,25 @@ import {

import { Socket } from 'net';
import { RHelpTopicProvider } from './help';
import { RLspOutputChannelManager } from './lsp-output-channel-manager';
import { R_DOCUMENT_SELECTORS } from './provider';
import { VirtualDocumentProvider } from './virtual-documents';

/**
* Global output channel for R LSP sessions
*
* Since we only have one LSP session active at any time, and since the start of
* a new session is logged with a session ID, we use a single output channel for
* all LSP sessions. Watch out for session start log messages to find the
* relevant section of the log.
*/
let _lspOutputChannel: vscode.OutputChannel | undefined;
function getLspOutputChannel(): vscode.OutputChannel {
if (!_lspOutputChannel) {
_lspOutputChannel = positron.window.createRawLogOutputChannel('R Language Server');
}
return _lspOutputChannel;
}

/**
* The state of the language server.
*/
Expand Down Expand Up @@ -107,12 +122,6 @@ export class ArkLsp implements vscode.Disposable {

const { notebookUri } = this._metadata;

// Persistant output channel, used across multiple sessions of the same name + mode combination
const outputChannel = RLspOutputChannelManager.instance.getOutputChannel(
this._dynState.sessionName,
this._metadata.sessionMode
);

const clientOptions: LanguageClientOptions = {
// If this client belongs to a notebook, set the document selector to only include that notebook.
// Otherwise, this is the main client for this language, so set the document selector to include
Expand All @@ -126,7 +135,7 @@ export class ArkLsp implements vscode.Disposable {
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.R')
},
errorHandler: new RErrorHandler(this._version, port),
outputChannel: outputChannel,
outputChannel: getLspOutputChannel(),
revealOutputChannelOn: RevealOutputChannelOn.Never,
middleware: {
handleDiagnostics(uri, diagnostics, next) {
Expand All @@ -146,7 +155,7 @@ export class ArkLsp implements vscode.Disposable {
const message = `Creating language client ${this._dynState.sessionName} for session ${this._metadata.sessionId} on port ${port}`;

LOGGER.info(message);
outputChannel.appendLine(message);
getLspOutputChannel().appendLine(message);

this.client = new LanguageClient(id, this.languageClientName, serverOptions, clientOptions);

Expand Down Expand Up @@ -319,10 +328,6 @@ export class ArkLsp implements vscode.Disposable {
}

public showOutput() {
const outputChannel = RLspOutputChannelManager.instance.getOutputChannel(
this._dynState.sessionName,
this._metadata.sessionMode
);
outputChannel.show();
getLspOutputChannel().show();
}
}
46 changes: 20 additions & 26 deletions extensions/positron-r/src/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@

import * as positron from 'positron';
import * as vscode from 'vscode';
import { RSession } from './session';
import { RSession, getActiveRSessions } from './session';

/**
* Manages all the R sessions. We keep our own references to each session in a
* singleton instance of this class so that we can invoke methods/check status
* directly, without going through Positron's API.
* Manages all the R sessions.
*/
export class RSessionManager implements vscode.Disposable {
/// Singleton instance
Expand All @@ -21,9 +19,6 @@ export class RSessionManager implements vscode.Disposable {
/// but we may improve on this in the future so it is good practice to track them.
private readonly _disposables: vscode.Disposable[] = [];

/// Map of session IDs to RSession instances
private _sessions: Map<string, RSession> = new Map();

/// The most recent foreground R session (foreground implies it is a console session)
private _lastForegroundSessionId: string | null = null;

Expand All @@ -50,18 +45,12 @@ export class RSessionManager implements vscode.Disposable {
}

/**
* Registers a runtime with the manager. Throws an error if a runtime with
* the same ID is already registered.
* Registers a runtime with the manager.
*
* @param id The runtime's ID
* @param runtime The runtime.
* @param session The session.
*/
setSession(sessionId: string, session: RSession): void {
if (this._sessions.has(sessionId)) {
throw new Error(`Session ${sessionId} already registered.`);
}
this._sessions.set(sessionId, session);
this._disposables.push(
setSession(session: RSession): void {
session.register(
session.onDidChangeRuntimeState(async (state) => {
await this.didChangeSessionRuntimeState(session, state);
})
Expand Down Expand Up @@ -99,18 +88,20 @@ export class RSessionManager implements vscode.Disposable {
return;
}

// TODO: Switch to `getActiveRSessions()` built on `positron.runtime.getActiveSessions()`
// and remove `this._sessions` entirely.
const session = this._sessions.get(sessionId);
const sessions = await getActiveRSessions();
const session = sessions.find(s => s.metadata.sessionId === sessionId);
if (!session) {
// The foreground session is for another language.
// The foreground session is for another language or was deactivated in the meantime
return;
}

if (session.metadata.sessionMode === positron.LanguageRuntimeSessionMode.Background) {
throw Error(`Foreground session with ID ${sessionId} must not be a background session.`);
}

// Multiple `activateConsoleSession()` might run concurrently if the
// `didChangeForegroundSession` event fires rapidly. We might want to queue
// the handling.
this._lastForegroundSessionId = session.metadata.sessionId;
await this.activateConsoleSession(session, 'foreground session changed');
}
Expand All @@ -120,7 +111,8 @@ export class RSessionManager implements vscode.Disposable {
*/
private async activateConsoleSession(session: RSession, reason: string): Promise<void> {
// Deactivate other console session servers first
await Promise.all(Array.from(this._sessions.values())
const sessions = await getActiveRSessions();
await Promise.all(sessions
.filter(s => {
return s.metadata.sessionId !== session.metadata.sessionId &&
s.metadata.sessionMode === positron.LanguageRuntimeSessionMode.Console;
Expand Down Expand Up @@ -152,9 +144,10 @@ export class RSessionManager implements vscode.Disposable {
*
* @returns The R console session, or undefined if there isn't one.
*/
getConsoleSession(): RSession | undefined {
async getConsoleSession(): Promise<RSession | undefined> {
const sessions = await getActiveRSessions();

// Sort the sessions by creation time (descending)
const sessions = Array.from(this._sessions.values());
sessions.sort((a, b) => b.created - a.created);

// Remove any sessions that aren't console sessions and have either
Expand Down Expand Up @@ -186,8 +179,9 @@ export class RSessionManager implements vscode.Disposable {
* @param sessionId The session identifier
* @returns The R session, or undefined if not found
*/
getSessionById(sessionId: string): RSession | undefined {
return this._sessions.get(sessionId);
async getSessionById(sessionId: string): Promise<RSession | undefined> {
const sessions = await getActiveRSessions();
return sessions.find(s => s.metadata.sessionId === sessionId);
}

/**
Expand Down
Loading