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
36 changes: 27 additions & 9 deletions src/languageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import net = require('net');
import url = require('url');
import { LanguageClient, LanguageClientOptions, StreamInfo, DocumentFilter, ErrorAction, CloseAction, RevealOutputChannelOn } from 'vscode-languageclient/node';
import { Disposable, workspace, Uri, TextDocument, WorkspaceConfiguration, OutputChannel, window, WorkspaceFolder } from 'vscode';
import { DisposableProcess, getRpath, exec } from './util';
import { DisposableProcess, getRpath, exec, isRPkgIntalled } from './util';

export class LanguageService implements Disposable {
private readonly clients: Map<string, LanguageClient> = new Map();
Expand All @@ -25,6 +25,16 @@ export class LanguageService implements Disposable {

private async createClient(config: WorkspaceConfiguration, selector: DocumentFilter[],
cwd: string, workspaceFolder: WorkspaceFolder, outputChannel: OutputChannel): Promise<LanguageClient> {
const installed = await isRPkgIntalled(
'languageserver', cwd, true,
'R package {languageserver} is required to enable R language service features such as code completion, function signature, find references, etc. Do you want to install it?',
'You may need to reopen an R file to start the language service after the package is installed.'
);

if (!installed) {
return undefined;
}

let client: LanguageClient;

const debug = config.get<boolean>('lsp.debug');
Expand Down Expand Up @@ -169,8 +179,10 @@ export class LanguageService implements Disposable {
];
const client = await self.createClient(config, documentSelector,
path.dirname(document.uri.fsPath), folder, outputChannel);
client.start();
self.clients.set(key, client);
if (client) {
client.start();
self.clients.set(key, client);
}
self.initSet.delete(key);
}
return;
Expand All @@ -188,8 +200,10 @@ export class LanguageService implements Disposable {
{ scheme: 'file', language: 'rmd', pattern: pattern },
];
const client = await self.createClient(config, documentSelector, folder.uri.fsPath, folder, outputChannel);
client.start();
self.clients.set(key, client);
if (client) {
client.start();
self.clients.set(key, client);
}
self.initSet.delete(key);
}

Expand All @@ -205,8 +219,10 @@ export class LanguageService implements Disposable {
{ scheme: 'untitled', language: 'rmd' },
];
const client = await self.createClient(config, documentSelector, os.homedir(), undefined, outputChannel);
client.start();
self.clients.set(key, client);
if (client) {
client.start();
self.clients.set(key, client);
}
self.initSet.delete(key);
}
return;
Expand All @@ -222,8 +238,10 @@ export class LanguageService implements Disposable {
];
const client = await self.createClient(config, documentSelector,
path.dirname(document.uri.fsPath), undefined, outputChannel);
client.start();
self.clients.set(key, client);
if (client) {
client.start();
self.clients.set(key, client);
}
self.initSet.delete(key);
}
return;
Expand Down
62 changes: 50 additions & 12 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,29 +310,36 @@ export async function executeRCommand(rCommand: string, fallBack?: string, cwd?:
const lim = '---vsc---';
const re = new RegExp(`${lim}(.*)${lim}`, 'ms');

const args = [
'--silent',
'--slave',
'--no-save',
'--no-restore',
];

const rPath = await getRpath(true);

const options: cp.ExecSyncOptionsWithStringEncoding = {
cwd: cwd,
encoding: 'utf-8'
};

const cmd = (
`${rPath} ${args.join(' ')} -e "cat('${lim}')" -e "${rCommand}" -e "cat('${lim}')"`
);
const args = [
'--silent',
'--slave',
'--no-save',
'--no-restore',
'-e', `cat('${lim}')`,
'-e', rCommand,
'-e', `cat('${lim}')`
];

let ret: string = undefined;

try {
const stdout = cp.execSync(cmd, options);
ret = stdout.replace(re, '$1');
const result = cp.spawnSync(rPath, args, options);
if (result.error) {
throw result.error;
}
const match = re.exec(result.stdout);
if (match.length === 2) {
ret = match[1];
} else {
throw new Error('Could not parse R output.');
}
} catch (e) {
if (fallBack) {
ret = fallBack;
Expand Down Expand Up @@ -449,3 +456,34 @@ export function exec(command: string, args?: ReadonlyArray<string>, options?: cp
});
return disposable;
}

/**
* Check if an R package is available or not
*
* @param name the R package name that need to be checked
* @returns a boolean Promise
*/
export async function isRPkgIntalled(name: string, cwd: string, promptToInstall: boolean = false, installMsg?: string, postInstallMsg?: string): Promise<boolean> {
const cmd = `cat(requireNamespace('${name}', quietly=TRUE))`;
const rOut = await executeRCommand(cmd, 'FALSE', cwd);
const isInstalled = rOut === 'TRUE';
if (promptToInstall && !isInstalled) {
if (installMsg === undefined) {
installMsg = `R package {${name}} is not installed. Do you want to install it?`;
}
void vscode.window.showErrorMessage(installMsg, 'Yes', 'No')
.then(async function (select) {
if (select === 'Yes') {
const repo = await getCranUrl('', cwd);
const rPath = await getRpath(false);
const args = ['--silent', '--slave', '-e', `install.packages('${name}', repos='${repo}')`];
void executeAsTask('Install Package', rPath, args, true);
if (postInstallMsg) {
void vscode.window.showInformationMessage(postInstallMsg, 'OK');
}
return true;
}
});
}
return isInstalled;
}