Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 33 additions & 49 deletions extensions/positron-r/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

import * as vscode from 'vscode';
import * as positron from 'positron';
import { delay } from './util';
import { timeout } from './util';
import { RRuntime } from './runtime';
import { getRunningRRuntime } from './provider';
import { getRPackageName } from './contexts';
import { getRPackageTasks } from './tasks';
import { randomUUID } from 'crypto';
Expand Down Expand Up @@ -56,57 +57,40 @@ export async function registerCommands(context: vscode.ExtensionContext, runtime
const packageName = await getRPackageName();
const tasks = await getRPackageTasks();
const task = tasks.filter(task => task.definition.task === 'r.task.packageInstall')[0];
const runningRuntimes = await positron.runtime.getRunningRuntimes('r');
if (!runningRuntimes || !runningRuntimes.length) {
vscode.window.showWarningMessage('Cannot install package as there is no R interpreter running.');
return;
}
// For now, there will be only one running R runtime:
const runtime = runtimes.get(runningRuntimes[0].runtimeId);

if (runtime) {
const execution = await vscode.tasks.executeTask(task);
const disp1 = vscode.tasks.onDidEndTaskProcess(async e => {
if (e.execution === execution) {
if (e.exitCode === 0) {
vscode.commands.executeCommand('workbench.panel.positronConsole.focus');
try {
await positron.runtime.restartLanguageRuntime(runtime.metadata.runtimeId);
} catch {
// If restarting promise rejects, dispose of listener:
disp1.dispose();
}

// A promise that resolves when the runtime is ready:
const promise = new Promise<void>(resolve => {
const disp2 = runtime.onDidChangeRuntimeState(runtimeState => {
if (runtimeState === positron.RuntimeState.Ready) {
resolve();
disp2.dispose();
}
});
});
const runtime = await getRunningRRuntime(runtimes);

const execution = await vscode.tasks.executeTask(task);
const disp1 = vscode.tasks.onDidEndTaskProcess(async e => {
if (e.execution === execution) {
if (e.exitCode === 0) {
vscode.commands.executeCommand('workbench.panel.positronConsole.focus');
try {
await positron.runtime.restartLanguageRuntime(runtime.metadata.runtimeId);
} catch {
// If restarting promise rejects, dispose of listener:
disp1.dispose();
}

// A promise that rejects after a timeout;
const timeout = new Promise<void>((_, reject) => {
setTimeout(() => {
reject(new Error('Timed out after 10 seconds waiting for R to be ready.'));
}, 1e4);
// A promise that resolves when the runtime is ready:
const promise = new Promise<void>(resolve => {
const disp2 = runtime.onDidChangeRuntimeState(runtimeState => {
if (runtimeState === positron.RuntimeState.Ready) {
resolve();
disp2.dispose();
}
});

// Wait for the the runtime to be ready, or for the timeout:
await Promise.race([promise, timeout]);
runtime.execute(`library(${packageName})`,
randomUUID(),
positron.RuntimeCodeExecutionMode.Interactive,
positron.RuntimeErrorBehavior.Continue);
}
disp1.dispose();
});

// Wait for the the runtime to be ready, or for a timeout:
await Promise.race([promise, timeout(1e4, 'waiting for R to be ready')]);
runtime.execute(`library(${packageName})`,
randomUUID(),
positron.RuntimeCodeExecutionMode.Interactive,
positron.RuntimeErrorBehavior.Continue);
}
});
} else {
throw new Error(`R runtime '${runningRuntimes[0].runtimeId}' is not registered in the extension host`);
}
disp1.dispose();
}
});
}),

vscode.commands.registerCommand('r.packageTest', () => {
Expand Down
4 changes: 4 additions & 0 deletions extensions/positron-r/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as vscode from 'vscode';
import * as positron from 'positron';

import { registerCommands } from './commands';
import { registerFormatter } from './formatting';
import { providePackageTasks } from './tasks';
import { setContexts } from './contexts';
import { discoverTests } from './testing';
Expand All @@ -29,6 +30,9 @@ export function activate(context: vscode.ExtensionContext) {
// Register commands.
registerCommands(context, runtimes);

// Register formatter.
registerFormatter(context, runtimes);

// Provide tasks.
providePackageTasks(context);

Expand Down
86 changes: 86 additions & 0 deletions extensions/positron-r/src/formatting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2023 Posit Software, PBC. All rights reserved.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as positron from 'positron';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { RRuntime, lastRuntimePath } from './runtime';
import { getRunningRRuntime } from './provider';
import { timeout } from './util';
import { randomUUID } from 'crypto';

export async function registerFormatter(context: vscode.ExtensionContext, runtimes: Map<string, RRuntime>) {

const rDocumentSelector = { scheme: 'file', language: 'r' } as vscode.DocumentSelector;

context.subscriptions.push(
vscode.languages.registerDocumentFormattingEditProvider(
rDocumentSelector,
new FormatterProvider(runtimes)
)
);
}

class FormatterProvider implements vscode.DocumentFormattingEditProvider {
constructor(public runtimes: Map<string, RRuntime>) { }

public provideDocumentFormattingEdits(document: vscode.TextDocument):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a future PR, I'll add another method for provideDocumentRangeFormattingEdits but I'd like to get this reviewed and merged first in a simpler state.

vscode.ProviderResult<vscode.TextEdit[]> {
return this.formatDocument(document, this.runtimes);
}

private async formatDocument(
document: vscode.TextDocument,
runtimes: Map<string, RRuntime>
): Promise<vscode.TextEdit[]> {
if (!lastRuntimePath) {
throw new Error(`No running R runtime to provide R package tasks.`);
}

const runtime = await getRunningRRuntime(runtimes);
const id = randomUUID();

// We can only use styler on files right now, so write the document to a temp file
const originalSource = document.getText();
const tempdir = os.tmpdir();
const fileToStyle = path.basename(document.fileName);
const stylerFile = path.join(tempdir, `styler-${fileToStyle}`);
fs.writeFileSync(stylerFile, originalSource);

// A promise that resolves when the runtime is idle:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sort of code makes me wish for an async API around these events. Then we'd do something like:

while (true) {
  if (await runtime.nextRuntimeMessage() === foo) {
    break
  }
}

Which I think makes the control flow much more apparent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That definitely would be nice. Do you want to open an issue describing that kind of change?

const promise = new Promise<void>(resolve => {
const disp = runtime.onDidReceiveRuntimeMessage(runtimeMessage => {
if (runtimeMessage.parent_id === id &&
runtimeMessage.type === positron.LanguageRuntimeMessageType.State) {
const runtimeMessageState = runtimeMessage as positron.LanguageRuntimeState;
if (runtimeMessageState.state === positron.RuntimeOnlineState.Idle) {
resolve();
disp.dispose();
}
}
});
});

// Actual formatting is done by styler
runtime.execute(
`styler::style_file('${stylerFile}')`,
id,
positron.RuntimeCodeExecutionMode.Silent,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently don't respect the silent execution mode. I've fixed this in posit-dev/ark#137

This means we shouldn't have to worry about setting the styler.quiet option, and in turn we can remove the withr dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding that! I will plan to merge this PR and then when I come back to the range formatter, I'll just double check that posit-dev/ark#137 solved the issue.

positron.RuntimeErrorBehavior.Continue);

// Wait for the the runtime to be idle, or for the timeout:
await Promise.race([promise, timeout(2e4, 'waiting for formatting')]);

// Read the now formatted file and then delete it
const formattedSource = fs.readFileSync(stylerFile).toString();
fs.promises.unlink(stylerFile);

// Return the formatted source
const fileStart = new vscode.Position(0, 0);
const fileEnd = document.lineAt(document.lineCount - 1).range.end;
return [new vscode.TextEdit(new vscode.Range(fileStart, fileEnd), formattedSource)];
}
}
13 changes: 3 additions & 10 deletions extensions/positron-r/src/lsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import * as vscode from 'vscode';
import * as positron from 'positron';
import { PromiseHandles } from './util';
import { PromiseHandles, timeout } from './util';
import { RStatementRangeProvider } from './statement-range';

import {
Expand Down Expand Up @@ -187,15 +187,8 @@ export class ArkLsp implements vscode.Disposable {
this._client!.stop();
});

// Don't wait more than a couple of seconds for the client to stop.
const timeout = new Promise<void>((_, reject) => {
setTimeout(() => {
reject(`Timed out after 2 seconds waiting for client to stop.`);
}, 2000);
});

// Wait for the client to enter the stopped state, or for the timeout
await Promise.race([promise, timeout]);
// Don't wait more than a couple of seconds for the client to stop
await Promise.race([promise, timeout(2000, 'waiting for client to stop')]);
}

/**
Expand Down
14 changes: 14 additions & 0 deletions extensions/positron-r/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,20 @@ export async function* rRuntimeProvider(
}
}


export async function getRunningRRuntime(runtimes: Map<string, RRuntime>): Promise<RRuntime> {
const runningRuntimes = await positron.runtime.getRunningRuntimes('r');
if (!runningRuntimes || !runningRuntimes.length) {
throw new Error('Cannot get running runtime as there is no R interpreter running.');
}
// For now, there will be only one running R runtime:
const runtime = runtimes.get(runningRuntimes[0].runtimeId);
if (!runtime) {
throw new Error(`R runtime '${runningRuntimes[0].runtimeId}' is not registered in the extension host`);
}
return runtime;
}

Comment on lines +281 to +293
Copy link
Contributor Author

@juliasilge juliasilge Nov 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatter is another example of us needing to work with the running runtime in the extension, at least for now while it uses styler. I abstracted out this function for the reusable pieces so now we:

  • find all the runtimes during registration
  • pass all of them around to wherever the extension needs
  • use this function to get the one that is running at any given moment

I don't think this is too bad for now but we might need to revisit this approach.

// directory where this OS is known to keep its R installations
function rHeadquarters(): string {
switch (process.platform) {
Expand Down
5 changes: 5 additions & 0 deletions extensions/positron-r/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

export function timeout(ms: number, reason: string) {
return new Promise((_, reject) => {
setTimeout(() => reject(`Timeout while ${reason}`), ms);
});
}

export function readLines(pth: string): Array<string> {
const bigString = fs.readFileSync(pth, 'utf8');
Expand Down