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
15 changes: 14 additions & 1 deletion R/help/helpServer.R
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
# get values from extension-set env values
lim <- Sys.getenv("VSCR_LIM")

NEW_PACKAGE_STRING <- "NEW_PACKAGES"

cat(
lim,
tools::startDynamicHelp(),
lim,
sep = ""
)

while (TRUE) Sys.sleep(1)
currentPackages <- NULL

while (TRUE) {
newPackages <- installed.packages(fields = "Packaged")[, c("Version", "Packaged")]
if (!identical(currentPackages, newPackages)) {
if (!is.null(currentPackages)) {
cat(NEW_PACKAGE_STRING, "\n")
}
currentPackages <- newPackages
}
Sys.sleep(1)
}
160 changes: 66 additions & 94 deletions src/helpViewer/helpProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface RHelpProviderOptions {
rPath: string;
// directory in which to launch R processes
cwd?: string;
// listener to notify when new packages are installed
pkgListener?: () => void;
}

// Class to forward help requests to a backgorund R instance that is running a help server
Expand All @@ -23,10 +25,12 @@ export class HelpProvider {
private port: number|Promise<number>;
private readonly rPath: string;
private readonly cwd?: string;
private readonly pkgListener?: () => void;

public constructor(options: RHelpProviderOptions){
this.rPath = options.rPath || 'R';
this.cwd = options.cwd;
this.pkgListener = options.pkgListener;
this.port = this.launchRHelpServer(); // is a promise for now!
}

Expand All @@ -37,7 +41,9 @@ export class HelpProvider {

public async launchRHelpServer(): Promise<number>{
const lim = '---vsc---';
const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'ms');
const portRegex = new RegExp(`.*${lim}(.*)${lim}.*`, 'ms');

const newPackageRegex = new RegExp('NEW_PACKAGES');

// starts the background help server and waits forever to keep the R process running
const scriptPath = extensionContext.asAbsolutePath('R/help/helpServer.R');
Expand All @@ -53,16 +59,21 @@ export class HelpProvider {

let str = '';
// promise containing the first output of the r process (contains only the port number)
const outputPromise = new Promise<string>((resolve) => {
const portPromise = new Promise<string>((resolve) => {
this.cp.stdout?.on('data', (data) => {
try{
// eslint-disable-next-line
str += data.toString();
} catch(e){
resolve('');
}
if(re.exec(str)){
resolve(str.replace(re, '$1'));
if(portRegex.exec(str)){
resolve(str.replace(portRegex, '$1'));
str = str.replace(portRegex, '');
}
if(newPackageRegex.exec(str)){
this.pkgListener?.();
str = str.replace(newPackageRegex, '');
}
});
this.cp.on('close', () => {
Expand All @@ -71,19 +82,18 @@ export class HelpProvider {
});

// await and store port number
const output = await outputPromise;
const port = Number(output);
const port = Number(await portPromise);

// is returned as a promise if not called with "await":
return port;
}

public async getHelpFileFromRequestPath(requestPath: string): Promise<null|rHelp.HelpFile> {
public async getHelpFileFromRequestPath(requestPath: string): Promise<undefined|rHelp.HelpFile> {
// make sure the server is actually running
this.port = await this.port;

if(!this.port || typeof this.port !== 'number'){
return null;
return undefined;
}

// remove leading '/'
Expand Down Expand Up @@ -174,17 +184,17 @@ interface PackageAliases {
[key: string]: string;
}
}
interface AllPackageAliases {
[key: string]: PackageAliases
}

// Implements the aliasProvider required by the help panel
export class AliasProvider {

private readonly rPath: string;
private readonly cwd?: string;
private readonly rScriptFile: string;
private allPackageAliases?: null | {
[key: string]: PackageAliases;
}
private aliases?: null | rHelp.Alias[];
private aliases?: undefined | rHelp.Alias[];
private readonly persistentState?: Memento;

constructor(args: AliasProviderArgs){
Expand All @@ -195,123 +205,85 @@ export class AliasProvider {
}

// delete stored aliases, will be generated on next request
public refresh(): void {
this.aliases = null;
this.allPackageAliases = null;
void this.persistentState?.update('r.helpPanel.cachedPackageAliases', undefined);
}

// get all aliases that match the given name, if specified only from 1 package
public getAliasesForName(name: string, pkgName?: string): rHelp.Alias[] | null {
const aliases = this.getAliasesForPackage(pkgName);
if(aliases){
return aliases.filter((v) => v.name === name);
} else{
return null;
}
public async refresh(): Promise<void> {
this.aliases = undefined;
await this.persistentState?.update('r.helpPanel.cachedAliases', undefined);
this.makeAllAliases();
}

// get a list of all aliases
public getAllAliases(): rHelp.Alias[] | null {
if(!this.aliases){
this.makeAllAliases();
}
return this.aliases || null;
}

// get all aliases, grouped by package
private getPackageAliases() {
if(!this.allPackageAliases){
this.readAliases();
public getAllAliases(): rHelp.Alias[] | undefined {
// try this.aliases:
if(this.aliases){
return this.aliases;
}
return this.allPackageAliases;
}

// get all aliases provided by one package
private getAliasesForPackage(pkgName?: string): rHelp.Alias[] | null {
if(!pkgName){
return this.getAllAliases();
}
const packageAliases = this.getPackageAliases();
if(packageAliases && pkgName in packageAliases){
const al = packageAliases[pkgName].aliases;
if(al){
const ret: rHelp.Alias[] = [];
for(const fncName in al){
ret.push({
name: fncName,
alias: al[fncName],
package: pkgName
});
}
return ret;
}

// try cached aliases:
const cachedAliases = this.persistentState?.get<rHelp.Alias[]>('r.helpPanel.cachedAliases');
if(cachedAliases){
this.aliases = cachedAliases;
return cachedAliases;
}
return null;

// try to make new aliases (returns undefined if unsuccessful):
const newAliases = this.makeAllAliases();
this.aliases = newAliases;
this.persistentState?.update('r.helpPanel.cachedAliases', newAliases);
return newAliases;
}

// converts aliases grouped by package to a flat list of aliases
private makeAllAliases(): void {
if(!this.allPackageAliases){
this.readAliases();
private makeAllAliases(): rHelp.Alias[] | undefined {
// get aliases from R (nested format)
const allPackageAliases = this.getAliasesFromR();
if(!allPackageAliases){
return undefined;
}
if(this.allPackageAliases){
const ret: rHelp.Alias[] = [];
for(const pkg in this.allPackageAliases){
const pkgName = this.allPackageAliases[pkg].package || pkg;
const al = this.allPackageAliases[pkg].aliases;
if(al){
for(const fncName in al){
ret.push({
name: fncName,
alias: al[fncName],
package: pkgName
});
}
}

// flatten aliases into one list:
const allAliases: rHelp.Alias[] = [];
for(const pkg in allPackageAliases){
const pkgName = allPackageAliases[pkg].package || pkg;
const pkgAliases = allPackageAliases[pkg].aliases || {};
for(const fncName in pkgAliases){
allAliases.push({
name: fncName,
alias: pkgAliases[fncName],
package: pkgName
});
}
this.aliases = ret;
} else{
this.aliases = null;
}
return allAliases;
}

// call R script `getAliases.R` and parse the output
private readAliases(): void {
// read from persistent workspace cache
const cachedAliases = this.persistentState?.get<{[key: string]: PackageAliases}>('r.helpPanel.cachedPackageAliases');
if(cachedAliases){
this.allPackageAliases = cachedAliases;
return;
}
private getAliasesFromR(): undefined | AllPackageAliases {

// get from R
this.allPackageAliases = null;
const lim = '---vsc---'; // must match the lim used in R!
const re = new RegExp(`^.*?${lim}(.*)${lim}.*$`, 'ms');
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-aliases-'));
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-aliases'));
const tempFile = path.join(tempDir, 'aliases.json');
const cmd = `${this.rPath} --silent --no-save --no-restore --slave -f "${this.rScriptFile}" > "${tempFile}"`;

let allPackageAliases: undefined | AllPackageAliases = undefined;
try{
// execute R script 'getAliases.R'
// aliases will be written to tempDir
// aliases will be written to tempFile
cp.execSync(cmd, {cwd: this.cwd});

// read and parse aliases
const txt = fs.readFileSync(tempFile, 'utf-8');
const json = txt.replace(re, '$1');
if(json){
this.allPackageAliases = <{[key: string]: PackageAliases}> JSON.parse(json) || {};
allPackageAliases = <{[key: string]: PackageAliases}> JSON.parse(json) || {};
}
} catch(e: unknown){
console.log(e);
void window.showErrorMessage((<{message: string}>e).message);
} finally {
fs.rmdirSync(tempDir, {recursive: true});
}
// update persistent workspace cache
void this.persistentState?.update('r.helpPanel.cachedPackageAliases', this.allPackageAliases);
return allPackageAliases;
}
}

15 changes: 11 additions & 4 deletions src/helpViewer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,11 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
constructor(options: HelpOptions){
this.webviewScriptFile = vscode.Uri.file(options.webviewScriptPath);
this.webviewStyleFile = vscode.Uri.file(options.webviewStylePath);
this.helpProvider = new HelpProvider(options);
const pkgListener = () => {
void console.log('Restarting Help Server...');
void this.refresh(true);
};
this.helpProvider = new HelpProvider({...options, pkgListener: pkgListener});
this.aliasProvider = new AliasProvider(options);
this.packageManager = new PackageManager({...options, rHelp: this});
this.treeViewWrapper = new HelpTreeWrapper(this);
Expand Down Expand Up @@ -207,11 +211,14 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
}

// refresh cached help info
public refresh(): boolean {
public async refresh(refreshTreeView: boolean = false): Promise<boolean> {
this.cachedHelpFiles.clear();
this.helpProvider?.refresh?.();
this.aliasProvider?.refresh?.();
this.packageManager?.refresh?.();
await this.aliasProvider?.refresh?.();
await this.packageManager?.refresh?.();
if(refreshTreeView){
this.treeViewWrapper.refreshPackageRootNode();
}
return true;
}

Expand Down
4 changes: 2 additions & 2 deletions src/helpViewer/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ export class PackageManager {

// Functions to force a refresh of listed packages
// Useful e.g. after installing/removing packages
public refresh(): void {
public async refresh(): Promise<void> {
await this.clearCachedFiles();
this.cranUrl = undefined;
this.pullFavoriteNames();
void this.clearCachedFiles();
}

// Funciton to clear only the cached files regarding an individual package etc.
Expand Down
12 changes: 8 additions & 4 deletions src/helpViewer/treeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export class HelpTreeWrapper {
listener(node);
}
}

public refreshPackageRootNode(): void {
this.helpViewProvider.rootItem?.pkgRootNode?.refresh();
}
}


Expand Down Expand Up @@ -164,7 +168,7 @@ abstract class Node extends vscode.TreeItem{
// Only internal commands are handled here, custom commands are implemented in _handleCommand!
public handleCommand(cmd: cmdName){
if(cmd === 'CALLBACK' && this.callBack){
this.callBack();
void this.callBack();
} else if(cmd === 'QUICKPICK'){
if(this.quickPickCommand){
this._handleCommand(this.quickPickCommand);
Expand All @@ -186,7 +190,7 @@ abstract class Node extends vscode.TreeItem{

// implement this to handle callBacks (simple clicks on a node)
// can also be implemented in _handleCommand('CALLBACK')
public callBack?(): void;
public callBack?(): void | Promise<void>;

// Shows a quickpick containing the children of a node
// If the picked child has children itself, another quickpick is shown
Expand Down Expand Up @@ -580,8 +584,8 @@ class RefreshNode extends MetaNode {
label = 'Clear Cached Index Files & Restart Help Server';
iconPath = new vscode.ThemeIcon('refresh');

callBack(){
this.rHelp.refresh();
async callBack(){
await this.rHelp.refresh();
this.parent.pkgRootNode.refresh();
}
}
Expand Down