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
34 changes: 16 additions & 18 deletions src/rmarkdown/knit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as fs from 'fs-extra';
import path = require('path');
import yaml = require('js-yaml');

import { RMarkdownManager, KnitWorkingDirectory } from './manager';
import { RMarkdownManager, KnitWorkingDirectory, DisposableProcess } from './manager';
import { runTextInTerm } from '../rTerminal';
import { rmdPreviewManager } from '../extension';

Expand All @@ -26,23 +26,21 @@ interface IYamlFrontmatter {
}

export class RMarkdownKnitManager extends RMarkdownManager {
private async renderDocument(rPath: string, docPath: string, docName: string, yamlParams: IYamlFrontmatter, outputFormat?: string) {
private async renderDocument(rDocumentPath: string, docPath: string, docName: string, yamlParams: IYamlFrontmatter, outputFormat?: string): Promise<DisposableProcess> {
const openOutfile: boolean = util.config().get<boolean>('rmarkdown.knit.openOutputFile') ?? false;
const knitWorkingDir = this.getKnitDir(knitDir, docPath);
const knitWorkingDirText = knitWorkingDir ? `'${knitWorkingDir}'` : `NULL`;
const knitCommand = await this.getKnitCommand(yamlParams, rPath, outputFormat);
this.rPath = await util.getRpath(true);
const knitCommand = await this.getKnitCommand(yamlParams, rDocumentPath, outputFormat);
this.rPath = await util.getRpath();

const lim = '---vsc---';
const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'gms');
const cmd = (
`${this.rPath} --silent --slave --no-save --no-restore ` +
`-e "knitr::opts_knit[['set']](root.dir = ${knitWorkingDirText})" ` +
`-e "cat('${lim}', ` +
const cmd =
`knitr::opts_knit[['set']](root.dir = ${knitWorkingDirText});` +
`cat('${lim}', ` +
`${knitCommand}, ` +
`'${lim}',` +
`sep='')"`
);
`sep='')`;

const callback = (dat: string) => {
const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1');
Expand All @@ -68,7 +66,7 @@ export class RMarkdownKnitManager extends RMarkdownManager {
return await this.knitWithProgress(
{
fileName: docName,
filePath: rPath,
filePath: rDocumentPath,
cmd: cmd,
rCmd: knitCommand,
rOutputFormat: outputFormat,
Expand Down Expand Up @@ -122,7 +120,7 @@ export class RMarkdownKnitManager extends RMarkdownManager {
`rmarkdown::render(${docPath})`;
}

return knitCommand.replace(/['"]/g, '\\"');
return knitCommand.replace(/['"]/g, '\'');
}

// check if the workspace of the document is a R Markdown site.
Expand Down Expand Up @@ -217,12 +215,12 @@ export class RMarkdownKnitManager extends RMarkdownManager {

const isSaved = await util.saveDocument(wad);
if (isSaved) {
let rPath = util.ToRStringLiteral(wad.fileName, '"');
let rDocumentPath = util.ToRStringLiteral(wad.fileName, '"');
let encodingParam = util.config().get<string>('source.encoding');
encodingParam = `encoding = "${encodingParam}"`;
rPath = [rPath, encodingParam].join(', ');
rDocumentPath = [rDocumentPath, encodingParam].join(', ');
if (echo) {
rPath = [rPath, 'echo = TRUE'].join(', ');
rDocumentPath = [rDocumentPath, 'echo = TRUE'].join(', ');
}

// allow users to opt out of background process
Expand All @@ -233,7 +231,7 @@ export class RMarkdownKnitManager extends RMarkdownManager {
} else {
this.busyUriStore.add(busyPath);
await this.renderDocument(
rPath,
rDocumentPath,
wad.uri.fsPath,
path.basename(wad.uri.fsPath),
this.getYamlFrontmatter(wad.uri.fsPath),
Expand All @@ -243,9 +241,9 @@ export class RMarkdownKnitManager extends RMarkdownManager {
}
} else {
if (outputFormat === undefined) {
void runTextInTerm(`rmarkdown::render(${rPath})`);
void runTextInTerm(`rmarkdown::render(${rDocumentPath})`);
} else {
void runTextInTerm(`rmarkdown::render(${rPath}, "${outputFormat}")`);
void runTextInTerm(`rmarkdown::render(${rDocumentPath}, '${outputFormat}')`);
}
}
}
Expand Down
70 changes: 52 additions & 18 deletions src/rmarkdown/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ export enum KnitWorkingDirectory {
workspaceRoot = 'workspace root',
}

export type DisposableProcess = cp.ChildProcessWithoutNullStreams & vscode.Disposable;

export interface IKnitRejection {
cp: DisposableProcess;
wasCancelled: boolean;
}

const rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown');

interface IKnitArgs {
filePath: string;
fileName: string;
Expand All @@ -18,13 +27,6 @@ interface IKnitArgs {
onRejection?: (...args: unknown[]) => unknown;
}

export interface IKnitRejection {
cp: cp.ChildProcessWithoutNullStreams;
wasCancelled: boolean;
}

const rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown');

export abstract class RMarkdownManager {
protected rPath: string = undefined;
protected rMarkdownOutput: vscode.OutputChannel = rMarkdownOutput;
Expand All @@ -51,18 +53,44 @@ export abstract class RMarkdownManager {
}
}

protected async knitDocument(args: IKnitArgs, token?: vscode.CancellationToken, progress?: vscode.Progress<unknown>): Promise<cp.ChildProcessWithoutNullStreams | IKnitRejection> {
protected async knitDocument(args: IKnitArgs, token?: vscode.CancellationToken, progress?: vscode.Progress<unknown>): Promise<DisposableProcess | IKnitRejection> {
// vscode.Progress auto-increments progress, so we use this
// variable to set progress to a specific number
let currentProgress = 0;
return await new Promise<cp.ChildProcessWithoutNullStreams>(
let printOutput = true;

return await new Promise<DisposableProcess>(
(resolve, reject) => {
const cmd = args.cmd;
const fileName = args.fileName;
let childProcess: cp.ChildProcessWithoutNullStreams;
const processArgs = [
`--silent`,
`--slave`,
`--no-save`,
`--no-restore`,
`-e`,
cmd
];
const processOptions = {
env: process.env
};

let childProcess: DisposableProcess;

try {
childProcess = cp.exec(cmd);
childProcess = util.asDisposable(
cp.spawn(
`${this.rPath}`,
processArgs,
processOptions
),
() => {
if (childProcess.kill('SIGKILL')) {
rMarkdownOutput.appendLine('[VSC-R] terminating R process');
printOutput = false;
}
}
);
progress.report({
increment: 0,
message: '0%'
Expand All @@ -81,9 +109,13 @@ export abstract class RMarkdownManager {
childProcess.stdout.on('data',
(data: Buffer) => {
const dat = data.toString('utf8');
this.rMarkdownOutput.appendLine(dat);
if (printOutput) {
this.rMarkdownOutput.appendLine(dat);

}
const percentRegex = /[0-9]+(?=%)/g;
const percentRegOutput = dat.match(percentRegex);

if (percentRegOutput) {
for (const item of percentRegOutput) {
const perc = Number(item);
Expand All @@ -108,7 +140,9 @@ export abstract class RMarkdownManager {

childProcess.stderr.on('data', (data: Buffer) => {
const dat = data.toString('utf8');
this.rMarkdownOutput.appendLine(dat);
if (printOutput) {
this.rMarkdownOutput.appendLine(dat);
}
});

childProcess.on('exit', (code, signal) => {
Expand All @@ -126,10 +160,10 @@ export abstract class RMarkdownManager {
);
}

protected async knitWithProgress(args: IKnitArgs): Promise<cp.ChildProcessWithoutNullStreams> {
let childProcess: cp.ChildProcessWithoutNullStreams = undefined;
protected async knitWithProgress(args: IKnitArgs): Promise<DisposableProcess> {
let childProcess: DisposableProcess = undefined;
await util.doWithProgress(async (token: vscode.CancellationToken, progress: vscode.Progress<unknown>) => {
childProcess = await this.knitDocument(args, token, progress) as cp.ChildProcessWithoutNullStreams;
childProcess = await this.knitDocument(args, token, progress) as DisposableProcess;
},
vscode.ProgressLocation.Notification,
`Knitting ${args.fileName} ${args.rOutputFormat ? 'to ' + args.rOutputFormat : ''} `,
Expand All @@ -140,8 +174,8 @@ export abstract class RMarkdownManager {
this.rMarkdownOutput.show(true);
}
// this can occur when a successfuly knitted document is later altered (while still being previewed) and subsequently fails to knit
args?.onRejection ? args.onRejection(args.filePath, rejection) :
rejection?.cp.kill('SIGKILL');
args?.onRejection?.(args.filePath, rejection);
rejection.cp?.dispose();
});
return childProcess;
}
Expand Down
29 changes: 12 additions & 17 deletions src/rmarkdown/preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@

import * as cp from 'child_process';
import * as vscode from 'vscode';
import * as fs from 'fs-extra';
import * as cheerio from 'cheerio';
Expand All @@ -11,11 +9,11 @@ import crypto = require('crypto');
import { config, readContent, setContext, escapeHtml, UriIcon, saveDocument, getRpath } from '../util';
import { extensionContext, tmpDir } from '../extension';
import { knitDir } from './knit';
import { IKnitRejection, RMarkdownManager } from './manager';
import { DisposableProcess, RMarkdownManager } from './manager';

class RMarkdownPreview extends vscode.Disposable {
title: string;
cp: cp.ChildProcessWithoutNullStreams;
cp: DisposableProcess;
panel: vscode.WebviewPanel;
resourceViewColumn: vscode.ViewColumn;
outputUri: vscode.Uri;
Expand All @@ -25,7 +23,7 @@ class RMarkdownPreview extends vscode.Disposable {
autoRefresh: boolean;
mtime: number;

constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel,
constructor(title: string, cp: DisposableProcess, panel: vscode.WebviewPanel,
resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, filePath: string,
RMarkdownPreviewManager: RMarkdownPreviewManager, useCodeTheme: boolean, autoRefresh: boolean) {
super(() => {
Expand Down Expand Up @@ -268,7 +266,7 @@ export class RMarkdownPreviewManager extends RMarkdownManager {
toUpdate?.cp?.kill('SIGKILL');

if (toUpdate) {
const childProcess: cp.ChildProcessWithoutNullStreams | void = await this.previewDocument(previewUri, toUpdate.title).catch(() => {
const childProcess: DisposableProcess | void = await this.previewDocument(previewUri, toUpdate.title).catch(() => {
void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.');
this.rMarkdownOutput.show(true);
this.previewStore.delete(previewUri);
Expand All @@ -283,26 +281,25 @@ export class RMarkdownPreviewManager extends RMarkdownManager {

}

private async previewDocument(filePath: string, fileName?: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn) {
private async previewDocument(filePath: string, fileName?: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn): Promise<DisposableProcess> {
const knitWorkingDir = this.getKnitDir(knitDir, filePath);
const knitWorkingDirText = knitWorkingDir ? `'${knitWorkingDir}'` : `NULL`;
this.rPath = await getRpath(true);
this.rPath = await getRpath();

const lim = '---vsc---';
const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'ms');
const outputFile = path.join(tmpDir(), crypto.createHash('sha256').update(filePath).digest('hex') + '.html');
const cmd = (
`${this.rPath} --silent --slave --no-save --no-restore ` +
`-e "knitr::opts_knit[['set']](root.dir = ${knitWorkingDirText})" ` +
`-e "cat('${lim}', rmarkdown::render(` +
`knitr::opts_knit[['set']](root.dir = ${knitWorkingDirText});` +
`cat('${lim}', rmarkdown::render(` +
`'${filePath.replace(/\\/g, '/')}',` +
`output_format = rmarkdown::html_document(),` +
`output_file = '${outputFile.replace(/\\/g, '/')}',` +
`intermediates_dir = '${tmpDir().replace(/\\/g, '/')}'), '${lim}',` +
`sep='')"`
`sep='')`
);

const callback = (dat: string, childProcess: cp.ChildProcessWithoutNullStreams) => {
const callback = (dat: string, childProcess: DisposableProcess) => {
const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1');
if (outputUrl) {
if (viewer !== undefined) {
Expand All @@ -322,11 +319,9 @@ export class RMarkdownPreviewManager extends RMarkdownManager {
return false;
};

const onRejected = (filePath: string, rejection: IKnitRejection) => {
const onRejected = (filePath: string) => {
if (this.previewStore.has(filePath)) {
this.previewStore.delete(filePath);
} else {
rejection.cp.kill('SIGKILL');
}
};

Expand All @@ -342,7 +337,7 @@ export class RMarkdownPreviewManager extends RMarkdownManager {
);
}

private openPreview(outputUri: vscode.Uri, filePath: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn, autoRefresh: boolean): void {
private openPreview(outputUri: vscode.Uri, filePath: string, title: string, cp: DisposableProcess, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn, autoRefresh: boolean): void {

const panel = vscode.window.createWebviewPanel(
'previewRmd',
Expand Down
17 changes: 17 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,20 @@ export class UriIcon {
this.light = vscode.Uri.file(path.join(extIconPath, 'light', id + '.svg'));
}
}

/**
* As Disposable.
*
* Create a dispose method for any given object, and push it to the
* extension subscriptions array
*
* @param {T} toDispose - the object to add dispose to
* @param {Function} disposeFunction - the method called when the object is disposed
* @returns returned object is considered types T and vscode.Disposable
*/
export function asDisposable<T>(toDispose: T, disposeFunction: (...args: unknown[]) => unknown): T & vscode.Disposable {
type disposeType = T & vscode.Disposable;
(toDispose as disposeType).dispose = () => disposeFunction();
extensionContext.subscriptions.push(toDispose as disposeType);
return toDispose as disposeType;
}