From f1eef75eabc34a9d7f16ca7435f55c59a0838d40 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 15 Jul 2021 18:07:20 +1000 Subject: [PATCH 01/60] Update r.rmarkdown.codeLensCommands Use an enum to restrict the input for the setting: r.rmarkdown.codeLensCommands --- package.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7ee4913e3..291ce781c 100644 --- a/package.json +++ b/package.json @@ -1193,13 +1193,25 @@ "r.rmarkdown.codeLensCommands": { "type": "array", "items": { - "type": "string" + "type": "string", + "enum": [ + "r.selectCurrentChunk", + "r.runCurrentChunk", + "r.runAboveChunks", + "r.runCurrentAndBelowChunks", + "r.runBelowChunks", + "r.runAllChunks", + "r.runPreviousChunk", + "r.runNextChunk", + "r.goToPreviousChunk", + "r.goToNextChunk" + ] }, "default": [ "r.runCurrentChunk", "r.runAboveChunks" ], - "description": "Customize RMarkdown CodeLens, which are inline commands/buttons e.g. 'Run Chunk' shown on the first line of each code chunk. Available commands:\n\nr.selectCurrentChunk\nr.runCurrentChunk\nr.runAboveChunks\nr.runCurrentAndBelowChunks\nr.runBelowChunks\nr.runAllChunks\nr.runPreviousChunk\nr.runNextChunk\nr.goToPreviousChunk\nr.goToNextChunk\n\n----------------------------------\nCustomize both the commands AND its orders (that is, CodeLens respect user-specified orders):" + "description": "Customize RMarkdown CodeLens, which are inline commands/buttons e.g. 'Run Chunk' shown on the first line of each code chunk. \nCustomize both the commands AND its orders (that is, CodeLens respect user-specified orders):" }, "r.rmarkdown.enableCodeLens": { "type": "boolean", From ed2d16e75a9473ba7bbb9d883a5c6b8621d66c24 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Fri, 19 Nov 2021 14:11:15 +1100 Subject: [PATCH 02/60] Initial handling of xarningan::inf_mr --- src/rmarkdown/knit.ts | 6 ++++++ src/rmarkdown/manager.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index 3bf0957a3..baccf5898 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -7,6 +7,7 @@ import yaml = require('js-yaml'); import { RMarkdownManager, KnitWorkingDirectory, DisposableProcess } from './manager'; import { runTextInTerm } from '../rTerminal'; import { extensionContext, rmdPreviewManager } from '../extension'; +import { showBrowser } from '../session'; export let knitDir: KnitWorkingDirectory = util.config().get('rmarkdown.knit.defaults.knitWorkingDirectory') ?? undefined; @@ -35,6 +36,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { const lim = '---vsc---'; const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'gms'); + const serveReg = /(http:\/\/127[0-9.:]*\/.*\.html)/g; const scriptValues = { 'VSCR_KNIT_DIR': knitWorkingDirText, 'VSCR_LIM': lim, @@ -43,6 +45,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { const callback = (dat: string) => { const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); + const serveUrl = serveReg.exec(dat)?.[0]?.replace(serveReg, '$1'); if (outputUrl) { if (openOutfile) { const outFile = vscode.Uri.file(outputUrl); @@ -53,6 +56,9 @@ export class RMarkdownKnitManager extends RMarkdownManager { } } return true; + } else if (serveUrl) { + void showBrowser(serveUrl, docName, 'Beside'); + return true; } else { return false; } diff --git a/src/rmarkdown/manager.ts b/src/rmarkdown/manager.ts index 490eb039c..ae23088e2 100644 --- a/src/rmarkdown/manager.ts +++ b/src/rmarkdown/manager.ts @@ -151,6 +151,11 @@ export abstract class RMarkdownManager { if (printOutput) { this.rMarkdownOutput.appendLine(dat); } + // for some reason, some knitting formats have the output + // of the file url in the stderr stream + if (args.callback(dat, childProcess)) { + resolve(childProcess); + } }); childProcess.on('exit', (code, signal) => { From 814481c381ac53fb1a8295be15e936029ec6d739 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 22 Nov 2021 14:34:39 +1100 Subject: [PATCH 03/60] Kill the child process! Now stops the child process when the webview is closed --- src/rmarkdown/knit.ts | 11 +++++++++-- src/session.ts | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index baccf5898..efab93d69 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -43,7 +43,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { 'VSCR_KNIT_COMMAND': knitCommand }; - const callback = (dat: string) => { + const callback = (dat: string, process: DisposableProcess) => { const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); const serveUrl = serveReg.exec(dat)?.[0]?.replace(serveReg, '$1'); if (outputUrl) { @@ -57,7 +57,14 @@ export class RMarkdownKnitManager extends RMarkdownManager { } return true; } else if (serveUrl) { - void showBrowser(serveUrl, docName, 'Beside'); + const browser = showBrowser(serveUrl, docName, 'Beside'); + void browser.then((value) => { + if (value) { + value.onDidDispose(() => { + process.kill('SIGTERM'); + }); + } + }); return true; } else { return false; diff --git a/src/session.ts b/src/session.ts index 4275f6c22..32c78f525 100644 --- a/src/session.ts +++ b/src/session.ts @@ -195,7 +195,7 @@ async function updateGlobalenv() { } } -export async function showBrowser(url: string, title: string, viewer: string | boolean): Promise { +export async function showBrowser(url: string, title: string, viewer: string | boolean): Promise { console.info(`[showBrowser] uri: ${url}, viewer: ${viewer.toString()}`); const uri = Uri.parse(url); if (viewer === false) { @@ -240,6 +240,7 @@ export async function showBrowser(url: string, title: string, viewer: string | b }); panel.iconPath = new UriIcon('globe'); panel.webview.html = getBrowserHtml(externalUri); + return (panel); } console.info('[showBrowser] Done'); } From 8a7a0437d4fd9859be0b1616d5dd824df45e4e0c Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 2 Dec 2021 15:54:22 +1100 Subject: [PATCH 04/60] Allow localhost --- src/rmarkdown/knit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index efab93d69..e88ecf67a 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -36,7 +36,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { const lim = '---vsc---'; const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'gms'); - const serveReg = /(http:\/\/127[0-9.:]*\/.*\.html)/g; + const serveReg = /(http:\/\/(localhost)?[0-9.:]*\/.*\.html)/g; const scriptValues = { 'VSCR_KNIT_DIR': knitWorkingDirText, 'VSCR_LIM': lim, From 7f1c92052d4fedbd799e9195be9da42f8b1478cf Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Wed, 28 Jul 2021 15:55:06 +0800 Subject: [PATCH 05/60] Check conflict extension (#733) --- src/extension.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 15c77268e..4b665b960 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -36,6 +36,11 @@ export let rMarkdownPreview: RMarkdownPreviewManager | undefined = undefined; // Called (once) when the extension is activated export async function activate(context: vscode.ExtensionContext): Promise { + if (vscode.extensions.getExtension('mikhail-arkhipov.r')) { + void vscode.window.showInformationMessage('The R Tools (Mikhail-Arkhipov.r) extension is enabled and will have conflicts with vscode-R. To use vscode-R, please disable or uninstall the extension.'); + void vscode.commands.executeCommand('workbench.extensions.search', '@installed R Tools'); + } + // create a new instance of RExtensionImplementation // is used to export an interface to the help panel // this export is used e.g. by vscode-r-debugger to show the help panel from within debug sessions From 294c473a3abfd796fbfdd48ba4d53ed48bf366a1 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 30 Jul 2021 21:49:38 +0800 Subject: [PATCH 06/60] Fix date filter in data viewer (#736) --- src/session.ts | 77 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/src/session.ts b/src/session.ts index c266e5d9a..4cf8547df 100644 --- a/src/session.ts +++ b/src/session.ts @@ -450,7 +450,45 @@ export async function getTableHtml(webview: Webview, file: string): Promise + `; +} + function isFromWorkspace(dir: string) { if (workspace.workspaceFolders === undefined) { const rel = path.relative(os.homedir(), dir); @@ -668,7 +704,7 @@ async function updateRequest(sessionStatusBarItem: StatusBarItem) { break; } case 'httpgd': { - if(request.url){ + if (request.url) { globalHttpgdManager?.showViewer(request.url); } break; From 4290447d4e926148f01ae3020e83e5995e427796 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Wed, 11 Aug 2021 16:11:19 +0000 Subject: [PATCH 09/60] Accessing VS Code settings in R (#743) * PoF accessing VSC settings ** proof of concept ** Load R-related vsc settings and set as R option * Update getSettings.R * Fiddle with configurations * Setup example usage * Write settings.json to .vscode-R folder * Source vscode settings first * Move import back to init, only replace ops if null * Update package.json * Descriptive names, move settings under categories * Change setting names * Load settings before attach * Refine load_settings * Fix * Update viewColumn setting Update the viewColumn item's description * Minor typos * Use expression mapping * Use quote list Co-authored-by: Kun Ren Co-authored-by: Manuel Hentschel <53863351+ManuelHentschel@users.noreply.github.com> --- R/vsc.R | 54 ++++++++++++++++++ package.json | 152 ++++++++++++++++++++++++++++++++++++++++++++++++- src/session.ts | 12 ++++ 3 files changed, 217 insertions(+), 1 deletion(-) diff --git a/R/vsc.R b/R/vsc.R index 3b3889b73..b759959e6 100644 --- a/R/vsc.R +++ b/R/vsc.R @@ -7,6 +7,59 @@ homedir <- Sys.getenv( dir_watcher <- Sys.getenv("VSCODE_WATCHER_DIR", file.path(homedir, ".vscode-R")) request_file <- file.path(dir_watcher, "request.log") request_lock_file <- file.path(dir_watcher, "request.lock") +settings_file <- file.path(dir_watcher, "settings.json") +user_options <- names(options()) + +load_settings <- function() { + if (!file.exists(settings_file)) { + return(FALSE) + } + + mapping <- quote(list( + vsc.use_httpgd = plot$useHttpgd, + vsc.show_object_size = workspaceViewer$showObjectSize, + vsc.rstudioapi = session$emulateRStudioAPI, + vsc.str.max.level = session$levelOfObjectDetail, + vsc.globalenv = session$watchGlobalEnvironment, + vsc.plot = session$viewers$viewColumn$plot, + vsc.browser = session$viewers$viewColumn$browser, + vsc.viewer = session$viewers$viewColumn$viewer, + vsc.page_viewer = session$viewers$viewColumn$pageViewer, + vsc.view = session$viewers$viewColumn$view, + vsc.helpPanel = session$viewers$viewColumn$helpPanel + )) + + vsc_settings <- tryCatch(jsonlite::read_json(settings_file), error = function(e) { + message("Error occurs when reading VS Code settings: ", conditionMessage(e)) + }) + + if (is.null(vsc_settings)) { + return(FALSE) + } + + ops <- eval(mapping, vsc_settings) + + # exclude options set by user on startup + ops <- ops[!(names(ops) %in% user_options)] + + # translate VS Code setting values to R option values + r_options <- lapply(ops, function(x) { + if (is.character(x) && length(x) == 1) { + switch(EXPR = x, + "Disable" = FALSE, + "Minimal" = 0, + "Detailed" = 2, + x + ) + } else { + x + } + }) + + options(r_options) +} + +load_settings() if (is.null(getOption("help_type"))) { options(help_type = "html") @@ -396,6 +449,7 @@ if (show_view) { } attach <- function() { + load_settings() if (rstudioapi_enabled()) { rstudioapi_util_env$update_addin_registry(addin_registry) } diff --git a/package.json b/package.json index 161d8e60f..3bfe79637 100644 --- a/package.json +++ b/package.json @@ -1326,11 +1326,156 @@ "default": true, "description": "Enable R session watcher. Required for workspace viewer and most features to work with an R session. Restart required to take effect." }, + "r.session.watchGlobalEnvironment": { + "type": "boolean", + "default": true, + "markdownDescription": "Watch the global environment to provide hover, autocompletions, and workspace viewer information. Changes the option `vsc.globalenv` in R. Requires `#r.sessionWatcher#` to be set to `true`." + }, + "r.session.levelOfObjectDetail": { + "type": "string", + "markdownDescription": "How much of the object to show on hover, autocompletion, and in the workspace viewer? Changes the option `vsc.str.max.level` in R. Requires `#r.sessionWatcher#` to be set to `true`.", + "default": "Minimal", + "enum": [ + "Minimal", + "Detailed" + ], + "enumDescriptions": [ + "Display literal values and object types only.", + "Display list content, data frame column values, and example values." + ] + }, + "r.session.emulateRStudioAPI": { + "type": "boolean", + "default": false, + "markdownDescription": "Emulate the RStudio API for addin support and other {rstudioapi} calls. Changes the option `vsc.rstudioapi` in R. Requires `#r.sessionWatcher#` to be set to `true`." + }, + "r.session.viewers.viewColumn": { + "type": "object", + "markdownDescription": "Which view column should R-related webviews be displayed? Requires `#r.sessionWatcher#` to be set to `true`.", + "default": { + "plot": "Two", + "browser": "Active", + "viewer": "Two", + "pageViewer": "Active", + "view": "Two", + "helpPanel": "Two" + }, + "properties": { + "plot": { + "type": "string", + "description": "Which view column to show the plot file on graphics update? \n\nChanges the option 'vsc.plot' in R.", + "enum": [ + "Two", + "Active", + "Beside", + "Disable" + ], + "enumDescriptions": [ + "Display plots in editor group 2.", + "Display plots in the active editor.", + "Display plots beside the active editor.", + "Do not use the VSCode-R plot viewer." + ], + "default": "Two" + }, + "browser": { + "type": "string", + "description": "Which view column to show the WebView triggered by browser (e.g. shiny apps)? \n\nChanges the option 'vsc.browser' in R.", + "enum": [ + "Two", + "Active", + "Beside", + "Disable" + ], + "enumDescriptions": [ + "Display browser in editor group 2.", + "Display browser in the active editor.", + "Display browser beside the active editor.", + "Do not use the VSCode-R browser." + ], + "default": "Active" + }, + "viewer": { + "type": "string", + "description": "Which view column to show the WebView triggered by viewer (e.g. htmlwidgets)? \n\nChanges the option 'vsc.viewer' in R.", + "enum": [ + "Two", + "Active", + "Beside", + "Disable" + ], + "enumDescriptions": [ + "Display viewer in editor group 2.", + "Display viewer in the active editor.", + "Display viewer beside the active editor.", + "Do not use the VSCode-R viewer." + ], + "default": "Two" + }, + "pageViewer": { + "type": "string", + "description": "Which view column to show the WebView triggered by the page viewer (e.g. profvis)? \n\nChanges the option 'vsc.page_viewer' in R.", + "enum": [ + "Two", + "Active", + "Beside", + "Disable" + ], + "enumDescriptions": [ + "Display page viewer in editor group 2.", + "Display page viewer in the active editor.", + "Display page viewer beside the active editor.", + "Do not use the VSCode-R page viewer." + ], + "default": "Active" + }, + "view": { + "type": "string", + "description": "Which view column to show the WebView triggered by View()? \n\nChanges the option 'vsc.plot' in R.", + "enum": [ + "Two", + "Active", + "Beside", + "Disable" + ], + "enumDescriptions": [ + "Display view output in editor group 2.", + "Display view output in the active editor.", + "Display view output beside the active editor.", + "Do not use the VSCode-R view command." + ], + "default": "Two" + }, + "helpPanel": { + "type": "string", + "description": "Which view column to show the WebView triggered by the help panel? \n\nChanges the option 'vsc.help_panel' in R.", + "enum": [ + "Two", + "Active", + "Beside", + "Disable" + ], + "enumDescriptions": [ + "Display help panel in editor group 2.", + "Display help panel in the active editor.", + "Display help panel beside the active editor.", + "Do not use the VSCode-R help panel." + ], + "default": "Two" + } + }, + "additionalProperties": false + }, "r.rtermSendDelay": { "type": "integer", "default": 8, "description": "Delay in milliseconds before sending each line to rterm (only applies if r.bracketedPaste is false)" }, + "r.workspaceViewer.showObjectSize": { + "type": "boolean", + "default": false, + "markdownDescription": "Show object size when hovering over a workspace viewer item. Changes the option `vsc.show_object_size` in R." + }, "r.workspaceViewer.removeHiddenItems": { "type": "boolean", "default": false, @@ -1361,6 +1506,11 @@ "default": false, "description": "Default boolean value for automatically sharing R browser ports with guests." }, + "r.plot.useHttpgd": { + "type": "boolean", + "default": false, + "markdownDescription": "Use the httpgd-based plot viewer instead of the base VSCode-R plot viewer. Changes the option `vsc.use_httpgd` in R." + }, "r.plot.defaults.colorTheme": { "type": "string", "default": "original", @@ -1482,4 +1632,4 @@ "ws": "^7.4.6", "vscode-languageclient": "^7.0.0" } -} +} \ No newline at end of file diff --git a/src/session.ts b/src/session.ts index a702bb90d..3fa108fed 100644 --- a/src/session.ts +++ b/src/session.ts @@ -46,6 +46,13 @@ export function deploySessionWatcher(extensionPath: string): void { const initPath = path.join(extensionPath, 'R', 'init.R'); const linkPath = path.join(homeExtDir(), 'init.R'); fs.writeFileSync(linkPath, `local(source("${initPath.replace(/\\/g, '\\\\')}", chdir = TRUE, local = TRUE))\n`); + + writeSettings(); + workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration('r')) { + writeSettings(); + } + }); } export function startRequestWatcher(sessionStatusBarItem: StatusBarItem): void { @@ -103,6 +110,11 @@ export function removeSessionFiles(): void { console.info('[removeSessionFiles] Done'); } +function writeSettings() { + const settingPath = path.join(homeExtDir(), 'settings.json'); + fs.writeFileSync(settingPath, JSON.stringify(config())); +} + function updateSessionWatcher() { console.info(`[updateSessionWatcher] PID: ${pid}`); console.info('[updateSessionWatcher] Create globalEnvWatcher'); From 829149bcc30cbee57cd6f7dd849797d0b072a87f Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 12 Aug 2021 14:29:59 +0800 Subject: [PATCH 10/60] Fix README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 75e9b03c1..8dc4e0fd5 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Go to the installation wiki pages ([Windows](https://github.com/REditorSupport/v * Snippets for R and R Markdown. -* [R Language Service](https://github.com/REditorSupport/vscode-R/wiki/R-Language-Service): Code completion, function signature, symbol highlight, document outline, formatting, definition, refere +* [R Language Service](https://github.com/REditorSupport/vscode-R/wiki/R-Language-Service): Code completion, function signature, symbol highlight, document outline, formatting, definition, diagnostics, references, and more. * [Interacting with R terminals](https://github.com/REditorSupport/vscode-R/wiki/Interacting-with-R-terminals): Sending code to terminals, running multiple terminals, working with remote servers. From 6978d5bc848d431fa1c0aede94e7dac7af167490 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 13 Aug 2021 22:22:03 +0800 Subject: [PATCH 11/60] Use .DollarNames with default pattern (#750) --- R/vsc.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/vsc.R b/R/vsc.R index b759959e6..b71c6e4ed 100644 --- a/R/vsc.R +++ b/R/vsc.R @@ -170,7 +170,7 @@ inspect_env <- function(env, cache) { } obj_names <- if (is.object(obj)) { - .DollarNames(obj) + .DollarNames(obj, pattern = "") } else if (is.recursive(obj)) { names(obj) } else NULL From b5a38fe37b38c549ea5514df1aff1aec6ad8b450 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Sun, 15 Aug 2021 12:20:32 +0000 Subject: [PATCH 12/60] Fix issues with c() in function args (#751) * Fixes #713 Uses the match pattern for functions from the textmate R grammar * Remove old rbox message --- syntax/r.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/syntax/r.json b/syntax/r.json index 99d1efc8f..11b528274 100644 --- a/syntax/r.json +++ b/syntax/r.json @@ -1,9 +1,4 @@ { - "information_for_contributors": [ - "This file has been converted from https://github.com/randy3k/R-Box/blob/master/syntax/R%20Extended.sublime-syntax", - "If you want to provide a fix or improvement, please create a pull request against the original repository.", - "Once accepted there, we are happy to receive an update request." - ], "fileTypes": [ "R", "r", @@ -340,8 +335,8 @@ "function-declarations": { "patterns": [ { - "begin": "^\\s*([a-zA-Z._][\\w.:]*)\\s*(< Date: Tue, 17 Aug 2021 14:50:59 +0800 Subject: [PATCH 13/60] Handle error in capture_str (#756) * Handle error in capture_str * Simplify capture_str --- R/vsc.R | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/R/vsc.R b/R/vsc.R index b71c6e4ed..dbcf92b11 100644 --- a/R/vsc.R +++ b/R/vsc.R @@ -88,15 +88,17 @@ request <- function(command, ...) { } capture_str <- function(object, max.level = getOption("vsc.str.max.level", 0)) { - paste0( - utils::capture.output( + tryCatch( + paste0(utils::capture.output( utils::str(object, max.level = max.level, give.attr = FALSE, vec.len = 1 ) - ), - collapse = "\n" + ), collapse = "\n"), + error = function(e) { + paste0(class(object), collapse = ", ") + } ) } @@ -153,7 +155,7 @@ inspect_env <- function(env, cache) { class = class(obj), type = scalar(typeof(obj)), length = scalar(length(obj)), - str = scalar(trimws(capture_str(obj)[[1L]])) + str = scalar(trimws(capture_str(obj))) ) if (show_object_size) { From 6f001c199668cf3ff457ea57cbc26cc1e0e21f1b Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 19 Aug 2021 17:06:37 +0000 Subject: [PATCH 14/60] Add icons to webviews (#759) * Add icons to webviews * Use class for icon sourcing - Use the UriIcon constructor to create icon uris * Remove unused icon - Removes the search.svg icon, as open-preview.svg was used instead --- images/icons/dark/globe.svg | 3 +++ images/icons/dark/graph.svg | 3 +++ images/icons/dark/help.svg | 3 +++ images/icons/dark/open-preview.svg | 3 +++ images/icons/dark/preview.svg | 3 +++ images/icons/light/globe.svg | 3 +++ images/icons/light/graph.svg | 3 +++ images/icons/light/help.svg | 10 ++++++++++ images/icons/light/open-preview.svg | 3 +++ images/icons/light/preview.svg | 3 +++ src/extension.ts | 1 + src/helpViewer/panel.ts | 11 +++++------ src/plotViewer/index.ts | 7 ++++--- src/rmarkdown/preview.ts | 5 ++++- src/session.ts | 7 +++++-- src/util.ts | 11 +++++++++++ 16 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 images/icons/dark/globe.svg create mode 100644 images/icons/dark/graph.svg create mode 100644 images/icons/dark/help.svg create mode 100644 images/icons/dark/open-preview.svg create mode 100644 images/icons/dark/preview.svg create mode 100644 images/icons/light/globe.svg create mode 100644 images/icons/light/graph.svg create mode 100644 images/icons/light/help.svg create mode 100644 images/icons/light/open-preview.svg create mode 100644 images/icons/light/preview.svg diff --git a/images/icons/dark/globe.svg b/images/icons/dark/globe.svg new file mode 100644 index 000000000..1ff60a534 --- /dev/null +++ b/images/icons/dark/globe.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/dark/graph.svg b/images/icons/dark/graph.svg new file mode 100644 index 000000000..e07277064 --- /dev/null +++ b/images/icons/dark/graph.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/dark/help.svg b/images/icons/dark/help.svg new file mode 100644 index 000000000..a0a00f005 --- /dev/null +++ b/images/icons/dark/help.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/dark/open-preview.svg b/images/icons/dark/open-preview.svg new file mode 100644 index 000000000..b4ba465da --- /dev/null +++ b/images/icons/dark/open-preview.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/dark/preview.svg b/images/icons/dark/preview.svg new file mode 100644 index 000000000..6f3c492bc --- /dev/null +++ b/images/icons/dark/preview.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/light/globe.svg b/images/icons/light/globe.svg new file mode 100644 index 000000000..a7ad58fe5 --- /dev/null +++ b/images/icons/light/globe.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/light/graph.svg b/images/icons/light/graph.svg new file mode 100644 index 000000000..05facbc1f --- /dev/null +++ b/images/icons/light/graph.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/light/help.svg b/images/icons/light/help.svg new file mode 100644 index 000000000..a6977ef94 --- /dev/null +++ b/images/icons/light/help.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/images/icons/light/open-preview.svg b/images/icons/light/open-preview.svg new file mode 100644 index 000000000..0e0e63c62 --- /dev/null +++ b/images/icons/light/open-preview.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/light/preview.svg b/images/icons/light/preview.svg new file mode 100644 index 000000000..2263883c6 --- /dev/null +++ b/images/icons/light/preview.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/extension.ts b/src/extension.ts index 29d70673f..e68b6e246 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -52,6 +52,7 @@ export async function activate(context: vscode.ExtensionContext): Promise('sessionWatcher'); + // register commands specified in package.json const commands = { // create R terminal diff --git a/src/helpViewer/panel.ts b/src/helpViewer/panel.ts index 9821d5928..a345f035d 100644 --- a/src/helpViewer/panel.ts +++ b/src/helpViewer/panel.ts @@ -4,8 +4,7 @@ import * as vscode from 'vscode'; import * as cheerio from 'cheerio'; import { HelpFile, RHelp } from '.'; -import { setContext } from '../util'; - +import { setContext, UriIcon } from '../util'; //// Declaration of interfaces used/implemented by the Help Panel class // specified when creating a new help panel @@ -73,7 +72,6 @@ export class HelpPanel { preserveFocus: preserveFocus }; this.panel = vscode.window.createWebviewPanel('rhelp', 'R Help', showOptions, webViewOptions); - this.initializePanel(); } @@ -82,8 +80,9 @@ export class HelpPanel { return this.panel.webview; } - - private initializePanel(){ + + private initializePanel() { + this.panel.iconPath = new UriIcon('help'); // virtual uris used to access local files this.webviewScriptUri = this.panel.webview.asWebviewUri(this.webviewScriptFile); this.webviewStyleUri = this.panel.webview.asWebviewUri(this.webviewStyleFile); @@ -109,7 +108,7 @@ export class HelpPanel { void this.setContextValues(); }); } - + public async setContextValues(): Promise { await setContext('r.helpPanel.active', !!this.panel?.active); diff --git a/src/plotViewer/index.ts b/src/plotViewer/index.ts index 0b5c91f85..2e1199245 100644 --- a/src/plotViewer/index.ts +++ b/src/plotViewer/index.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import * as fs from 'fs'; import * as ejs from 'ejs'; -import { config, setContext } from '../util'; +import { config, setContext, UriIcon } from '../util'; import { extensionContext } from '../extension'; @@ -272,7 +272,7 @@ export class HttpgdViewer implements IHttpgdViewer { readonly defaultPreviewPlotLayout: PreviewPlotLayout = 'multirow'; previewPlotLayout: PreviewPlotLayout; - + readonly defaultFullWindow: boolean = false; fullWindow: boolean; @@ -480,7 +480,7 @@ export class HttpgdViewer implements IHttpgdViewer { }; this.postWebviewMessage(msg); } - + public toggleFullWindow(force?: boolean): void { this.fullWindow = force ?? !this.fullWindow; const msg: ToggleFullWindowMessage = { @@ -745,6 +745,7 @@ export class HttpgdViewer implements IHttpgdViewer { showOptions || this.showOptions, this.webviewOptions ); + webviewPanel.iconPath = new UriIcon('graph'); webviewPanel.onDidDispose(() => this.webviewPanel = undefined); webviewPanel.onDidChangeViewState(() => { void this.setContextValues(); diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 53932ad9c..f721667bf 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -8,7 +8,7 @@ import path = require('path'); import crypto = require('crypto'); -import { config, doWithProgress, getRpath, readContent, setContext, escapeHtml } from '../util'; +import { config, doWithProgress, getRpath, readContent, setContext, escapeHtml, UriIcon } from '../util'; import { extensionContext, tmpDir } from '../extension'; class RMarkdownPreview extends vscode.Disposable { @@ -377,6 +377,7 @@ export class RMarkdownPreviewManager { } private openPreview(outputUri: vscode.Uri, filePath: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn, autoRefresh: boolean): void { + const panel = vscode.window.createWebviewPanel( 'previewRmd', `Preview ${title}`, @@ -391,6 +392,8 @@ export class RMarkdownPreviewManager { localResourceRoots: [vscode.Uri.file(tmpDir())], }); + panel.iconPath = new UriIcon('preview'); + // Push the new rmd webview to the open proccesses array, // to keep track of running child processes // (primarily used in killing the child process, but also diff --git a/src/session.ts b/src/session.ts index 3fa108fed..6af47dbf6 100644 --- a/src/session.ts +++ b/src/session.ts @@ -11,7 +11,7 @@ import { commands, StatusBarItem, Uri, ViewColumn, Webview, window, workspace, e import { runTextInTerm } from './rTerminal'; import { FSWatcher } from 'fs-extra'; -import { config, readContent } from './util'; +import { config, readContent, UriIcon } from './util'; import { purgeAddinPickerItems, dispatchRStudioAPICall } from './rstudioapi'; import { homeExtDir, rWorkspace, globalRHelp, globalHttpgdManager, extensionContext } from './extension'; @@ -237,6 +237,7 @@ export async function showBrowser(url: string, title: string, viewer: string | b } void commands.executeCommand('setContext', 'r.browser.active', false); }); + panel.iconPath = new UriIcon('globe'); panel.webview.html = getBrowserHtml(externalUri); } console.info('[showBrowser] Done'); @@ -297,7 +298,7 @@ export async function showWebView(file: string, title: string, viewer: string | retainContextWhenHidden: true, localResourceRoots: [Uri.file(dir), Uri.file(webviewDir)], }); - + panel.iconPath = new UriIcon('globe'); panel.webview.html = await getWebviewHtml(panel.webview, file, title, dir, webviewDir); } console.info('[showWebView] Done'); @@ -323,6 +324,7 @@ export async function showDataView(source: string, type: string, title: string, localResourceRoots: [Uri.file(resDir)], }); const content = await getTableHtml(panel.webview, file); + panel.iconPath = new UriIcon('open-preview'); panel.webview.html = content; } else if (source === 'list') { const panel = window.createWebviewPanel('dataview', title, @@ -337,6 +339,7 @@ export async function showDataView(source: string, type: string, title: string, localResourceRoots: [Uri.file(resDir)], }); const content = await getListHtml(panel.webview, file); + panel.iconPath = new UriIcon('open-preview'); panel.webview.html = content; } else { if (isGuestSession) { diff --git a/src/util.ts b/src/util.ts index b961cf268..c392a8953 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,6 +7,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as cp from 'child_process'; import { rGuestService, isGuestSession } from './liveShare'; +import { extensionContext } from './extension'; export function config(): vscode.WorkspaceConfiguration { return vscode.workspace.getConfiguration('r'); @@ -361,3 +362,13 @@ export function getDir(dirPath: string): string { } return dirPath; } + +export class UriIcon { + dark: vscode.Uri; + light: vscode.Uri; + constructor(id: string) { + const extIconPath = extensionContext.asAbsolutePath('images/icons'); + this.dark = vscode.Uri.file(path.join(extIconPath, 'dark', id + '.svg')); + this.light = vscode.Uri.file(path.join(extIconPath, 'light', id + '.svg')); + } +} From 31cd9df72a0eaed1da6b7aa96ecb3d32744ac18e Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Sat, 21 Aug 2021 09:24:13 +0800 Subject: [PATCH 15/60] release 2.2.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ccc2de6..017ddca07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ You can check all of our changes from [Release Page](https://github.com/REditorSupport/vscode-R/releases) +## [2.2.0](https://github.com/REditorSupport/vscode-R/releases/tag/v2.2.0) + +New Features + +* VS Code settings are now accessible from R and all vscode-specifc R options (`vsc.*`) now have +corresponding VS Code settings. (#743) + +Enhancements + +* Check conflict extension `mikhail-arkhipov.r` on activation. (#733) +* Add icons to WebViews. (#759) + +Fixes + +* Fix date filter in data viewer. (#736) +* Fix htmlwidget resource path in WebView. (#739) +* Use `.DollarNames` with default pattern. (#750) +* Fix syntax highlighting for `c()` in function args. (#751) +* Handle error in `capture_str()`. (#756) + ## [2.1.0](https://github.com/REditorSupport/vscode-R/releases/tag/v2.1.0) Important changes diff --git a/package.json b/package.json index 3bfe79637..be8640487 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "r", "displayName": "R", "description": "R Extension for Visual Studio Code", - "version": "2.1.0", + "version": "2.2.0", "author": "REditorSupport", "license": "SEE LICENSE IN LICENSE", "publisher": "Ikuyadeu", @@ -1632,4 +1632,4 @@ "ws": "^7.4.6", "vscode-languageclient": "^7.0.0" } -} \ No newline at end of file +} From d8306664ab302c4ebbeba08c082aca968c3e5eb9 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Fri, 3 Sep 2021 04:26:03 +0000 Subject: [PATCH 16/60] Enable rstudioapi by default (#769) --- R/vsc.R | 2 +- package.json | 2 +- src/rstudioapi.ts | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/R/vsc.R b/R/vsc.R index dbcf92b11..43e0cff83 100644 --- a/R/vsc.R +++ b/R/vsc.R @@ -578,7 +578,7 @@ options( # rstudioapi rstudioapi_enabled <- function() { - isTRUE(getOption("vsc.rstudioapi")) + isTRUE(getOption("vsc.rstudioapi", TRUE)) } if (rstudioapi_enabled()) { diff --git a/package.json b/package.json index be8640487..937cfd5e7 100644 --- a/package.json +++ b/package.json @@ -1346,7 +1346,7 @@ }, "r.session.emulateRStudioAPI": { "type": "boolean", - "default": false, + "default": true, "markdownDescription": "Emulate the RStudio API for addin support and other {rstudioapi} calls. Changes the option `vsc.rstudioapi` in R. Requires `#r.sessionWatcher#` to be set to `true`." }, "r.session.viewers.viewColumn": { diff --git a/src/rstudioapi.ts b/src/rstudioapi.ts index 9f0a200ab..ffc1cafc5 100644 --- a/src/rstudioapi.ts +++ b/src/rstudioapi.ts @@ -242,7 +242,7 @@ export function projectPath(): { path: string; } { } } - // if we got to here either: + // if we got to here either: // - the workspaceFolders array was undefined (no folder open) // - the activeText editor was an unsaved document, which has undefined workspace folder. // return undefined and handle with a message in R. @@ -317,10 +317,12 @@ export function purgeAddinPickerItems(): void { export async function launchAddinPicker(): Promise { if (!config().get('sessionWatcher')) { - throw ('{rstudioapi} emulation requires session watcher to be enabled in extension config.'); + void window.showErrorMessage('{rstudioapi} emulation requires session watcher to be enabled in extension config.'); + return; } if (!sessionDirectoryExists()) { - throw ('No active R terminal session, attach one to use RStudio addins.'); + void window.showErrorMessage('No active R terminal session, attach one to use RStudio addins.'); + return; } const addinPickerOptions: QuickPickOptions = { From 8da392361bfd95c4c93702b57755f28369320e22 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 3 Sep 2021 18:36:43 +0800 Subject: [PATCH 17/60] Use unsafe-inline for script-src (#771) --- src/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index 6af47dbf6..2bcc9a7c9 100644 --- a/src/session.ts +++ b/src/session.ts @@ -632,7 +632,7 @@ export async function getWebviewHtml(webview: Webview, file: string, title: stri upgrade-insecure-requests; default-src https: data: filesystem:; style-src https: data: filesystem: 'unsafe-inline'; - script-src https: data: filesystem: 'unsafe-eval'; + script-src https: data: filesystem: 'unsafe-inline'; `; return ` From a374935410b293501643483e43598df07dbec078 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Sat, 4 Sep 2021 13:18:37 +0000 Subject: [PATCH 18/60] R Markdown Enhancements (Knit Manager) (#765) * background knitting (with progress bar) * set knit working directory * Smart knit button, use custom knit functions + rmarkdown::render_site where appropriate --- package.json | 79 +++++++++++- src/extension.ts | 36 +++--- src/rTerminal.ts | 19 --- src/rmarkdown/index.ts | 102 ++++++++-------- src/rmarkdown/knit.ts | 253 +++++++++++++++++++++++++++++++++++++++ src/rmarkdown/manager.ts | 148 +++++++++++++++++++++++ src/rmarkdown/preview.ts | 205 +++++++++++++------------------ src/util.ts | 7 +- yarn.lock | 12 ++ 9 files changed, 644 insertions(+), 217 deletions(-) create mode 100644 src/rmarkdown/knit.ts create mode 100644 src/rmarkdown/manager.ts diff --git a/package.json b/package.json index 937cfd5e7..483f0c24e 100644 --- a/package.json +++ b/package.json @@ -521,6 +521,12 @@ "category": "R", "command": "r.goToNextChunk" }, + { + "command": "r.rmarkdown.setKnitDirectory", + "title": "R: Set Knit directory", + "icon": "$(zap)", + "category": "R Markdown" + }, { "command": "r.rmarkdown.showPreviewToSide", "title": "Open Preview to the Side", @@ -867,7 +873,7 @@ "command": "r.runSource" }, { - "when": "editorLangId == rmd", + "when": "editorLangId == rmd && editorFocus", "command": "r.knitRmd", "group": "navigation" } @@ -1006,8 +1012,18 @@ { "command": "r.rmarkdown.showPreviewToSide", "alt": "r.rmarkdown.showPreview", - "when": "editorLangId == rmd", + "when": "editorLangId == rmd && editorFocus", "group": "navigation" + }, + { + "submenu": "r.knitCommands", + "when": "editorLangId == rmd && editorFocus", + "group": "@1" + }, + { + "command": "r.rmarkdown.setKnitDirectory", + "when": "editorLangId == rmd && editorFocus", + "group": "@1" } ], "editor/context": [ @@ -1149,8 +1165,29 @@ "when": "resourceLangId == rmd", "group": "navigation" } + ], + "r.knitCommands": [ + { + "command": "r.knitRmd" + }, + { + "command": "r.knitRmdToPdf" + }, + { + "command": "r.knitRmdToHtml" + }, + { + "command": "r.knitRmdToAll" + } ] }, + "submenus": [ + { + "id": "r.knitCommands", + "label": "R: Knit", + "icon": "$(zap)" + } + ], "configuration": { "type": "object", "title": "R", @@ -1262,7 +1299,37 @@ "r.rmarkdown.preview.autoRefresh": { "type": "boolean", "default": true, - "description": "Enable automatic refresh of R Markdown preview on file update." + "markdownDescription": "Enable automatic refresh of R Markdown preview on file update." + }, + "r.rmarkdown.knit.useBackgroundProcess": { + "type": "boolean", + "default": true, + "markdownDescription": "Should knitting occur in a background process (*smart knitting*), or should it be done in the current R terminal (*manual knitting*)? \n\n*Smart knitting* includes additional features, such as custom knit function detection, R Markdown site detection, progress bars, and the setting knit directory." + }, + "r.rmarkdown.knit.focusOutputChannel": { + "type": "boolean", + "default": true, + "markdownDescription": "Should the R Markdown output channel be focused when knitting?\n\nRequires `#r.rmarkdown.knit.useBackgroundProcess#` to be set to `true`." + }, + "r.rmarkdown.knit.openOutputFile": { + "type": "boolean", + "default": false, + "markdownDescription": "Should the output file be opened automatically when using knit?\n\nRequires `#r.rmarkdown.knit.useBackgroundProcess#` to be set to `true`." + }, + "r.rmarkdown.knit.defaults.knitWorkingDirectory": { + "type": "string", + "default": "document directory", + "enum": [ + "document directory", + "workspace root" + ], + "enumDescriptions": [ + "Use the document's directory as the knit directory", + "Use the workspace root as the knit directory" + ], + "markdownDescription": "What working directory should R Markdown chunks be evaluated in? Default knit behaviour is to use the document's directory as root.\n\nRequires `#r.rmarkdown.knit.useBackgroundProcess#` to be set to `true`.", + "additionalItems": false, + "additionalProperties": false }, "r.helpPanel.enableSyntaxHighlighting": { "type": "boolean", @@ -1592,6 +1659,7 @@ "@types/express": "^4.17.12", "@types/fs-extra": "^9.0.11", "@types/highlight.js": "^10.1.0", + "@types/js-yaml": "^4.0.2", "@types/mocha": "^8.2.2", "@types/node": "^14.17.3", "@types/node-fetch": "^2.5.10", @@ -1623,13 +1691,14 @@ "highlight.js": "^10.7.2", "jquery": "^3.6.0", "jquery.json-viewer": "^1.4.0", + "js-yaml": "^4.1.0", "node-fetch": "^2.6.1", "popper.js": "^1.16.1", "showdown": "^1.9.1", "tree-kill": "^1.2.2", + "vscode-languageclient": "^7.0.0", "vsls": "^1.0.3015", "winreg": "^1.2.4", - "ws": "^7.4.6", - "vscode-languageclient": "^7.0.0" + "ws": "^7.4.6" } } diff --git a/src/extension.ts b/src/extension.ts index e68b6e246..4656413ee 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -22,7 +22,6 @@ import * as rShare from './liveShare'; import * as httpgdViewer from './plotViewer'; import * as languageService from './languageService'; -import { RMarkdownPreviewManager } from './rmarkdown/preview'; // global objects used in other files export const homeExtDir = (): string => util.getDir(path.join(os.homedir(), '.vscode-R')); @@ -32,7 +31,8 @@ export let globalRHelp: rHelp.RHelp | undefined = undefined; export let extensionContext: vscode.ExtensionContext; export let enableSessionWatcher: boolean = undefined; export let globalHttpgdManager: httpgdViewer.HttpgdManager | undefined = undefined; -export let rMarkdownPreview: RMarkdownPreviewManager | undefined = undefined; +export let rmdPreviewManager: rmarkdown.RMarkdownPreviewManager | undefined = undefined; +export let rmdKnitManager: rmarkdown.RMarkdownKnitManager | undefined = undefined; // Called (once) when the extension is activated export async function activate(context: vscode.ExtensionContext): Promise { @@ -51,6 +51,8 @@ export async function activate(context: vscode.ExtensionContext): Promise('sessionWatcher'); + rmdPreviewManager = new rmarkdown.RMarkdownPreviewManager(); + rmdKnitManager = new rmarkdown.RMarkdownKnitManager(); // register commands specified in package.json @@ -75,10 +77,10 @@ export async function activate(context: vscode.ExtensionContext): Promise { void rTerminal.runSource(true); }, // rmd related - 'r.knitRmd': () => { void rTerminal.knitRmd(false, undefined); }, - 'r.knitRmdToPdf': () => { void rTerminal.knitRmd(false, 'pdf_document'); }, - 'r.knitRmdToHtml': () => { void rTerminal.knitRmd(false, 'html_document'); }, - 'r.knitRmdToAll': () => { void rTerminal.knitRmd(false, 'all'); }, + 'r.knitRmd': () => { void rmdKnitManager.knitRmd(false, undefined); }, + 'r.knitRmdToPdf': () => { void rmdKnitManager.knitRmd(false, 'pdf_document'); }, + 'r.knitRmdToHtml': () => { void rmdKnitManager.knitRmd(false, 'html_document'); }, + 'r.knitRmdToAll': () => { void rmdKnitManager.knitRmd(false, 'all'); }, 'r.selectCurrentChunk': rmarkdown.selectCurrentChunk, 'r.runCurrentChunk': rmarkdown.runCurrentChunk, 'r.runPreviousChunk': rmarkdown.runPreviousChunk, @@ -91,14 +93,15 @@ export async function activate(context: vscode.ExtensionContext): Promise rMarkdownPreview.previewRmd(vscode.ViewColumn.Beside), - 'r.rmarkdown.showPreview': (uri: vscode.Uri) => rMarkdownPreview.previewRmd(vscode.ViewColumn.Active, uri), - 'r.rmarkdown.preview.refresh': () => rMarkdownPreview.updatePreview(), - 'r.rmarkdown.preview.openExternal': () => void rMarkdownPreview.openExternalBrowser(), - 'r.rmarkdown.preview.showSource': () => rMarkdownPreview.showSource(), - 'r.rmarkdown.preview.toggleStyle': () => rMarkdownPreview.toggleTheme(), - 'r.rmarkdown.preview.enableAutoRefresh': () => rMarkdownPreview.enableAutoRefresh(), - 'r.rmarkdown.preview.disableAutoRefresh': () => rMarkdownPreview.disableAutoRefresh(), + 'r.rmarkdown.setKnitDirectory': () => rmdKnitManager.setKnitDir(), + 'r.rmarkdown.showPreviewToSide': () => rmdPreviewManager.previewRmd(vscode.ViewColumn.Beside), + 'r.rmarkdown.showPreview': (uri: vscode.Uri) => rmdPreviewManager.previewRmd(vscode.ViewColumn.Active, uri), + 'r.rmarkdown.preview.refresh': () => rmdPreviewManager.updatePreview(), + 'r.rmarkdown.preview.openExternal': () => void rmdPreviewManager.openExternalBrowser(), + 'r.rmarkdown.preview.showSource': () => rmdPreviewManager.showSource(), + 'r.rmarkdown.preview.toggleStyle': () => rmdPreviewManager.toggleTheme(), + 'r.rmarkdown.preview.enableAutoRefresh': () => rmdPreviewManager.enableAutoRefresh(), + 'r.rmarkdown.preview.disableAutoRefresh': () => rmdPreviewManager.disableAutoRefresh(), // editor independent commands 'r.createGitignore': rGitignore.createGitignore, @@ -166,10 +169,6 @@ export async function activate(context: vscode.ExtensionContext): Promise { } } -export async function knitRmd(echo: boolean, outputFormat?: string): Promise { - const wad: vscode.TextDocument = vscode.window.activeTextEditor.document; - const isSaved = await util.saveDocument(wad); - if (isSaved) { - let rPath = util.ToRStringLiteral(wad.fileName, '"'); - let encodingParam = util.config().get('source.encoding'); - encodingParam = `encoding = "${encodingParam}"`; - rPath = [rPath, encodingParam].join(', '); - if (echo) { - rPath = [rPath, 'echo = TRUE'].join(', '); - } - if (outputFormat === undefined) { - void runTextInTerm(`rmarkdown::render(${rPath})`); - } else { - void runTextInTerm(`rmarkdown::render(${rPath}, "${outputFormat}")`); - } - } -} - export async function runSelection(): Promise { await runSelectionInTerm(true); } diff --git a/src/rmarkdown/index.ts b/src/rmarkdown/index.ts index 2210e3a66..bd981fd86 100644 --- a/src/rmarkdown/index.ts +++ b/src/rmarkdown/index.ts @@ -1,12 +1,12 @@ -import { - CancellationToken, CodeLens, CodeLensProvider, - CompletionItem, CompletionItemProvider, - Event, EventEmitter, Position, Range, TextDocument, TextEditorDecorationType, window, Selection, commands -} from 'vscode'; +import * as vscode from 'vscode'; import { runChunksInTerm } from '../rTerminal'; import { config } from '../util'; -function isRDocument(document: TextDocument) { +// reexports +export { knitDir, RMarkdownKnitManager } from './knit'; +export { RMarkdownPreviewManager } from './preview'; + +function isRDocument(document: vscode.TextDocument) { return (document.languageId === 'r'); } @@ -42,23 +42,23 @@ function getChunkEval(chunkOptions: string) { return (!chunkOptions.match(/eval\s*=\s*(F|FALSE)/g)); } -export class RMarkdownCodeLensProvider implements CodeLensProvider { - private codeLenses: CodeLens[] = []; - private _onDidChangeCodeLenses: EventEmitter = new EventEmitter(); - private readonly decoration: TextEditorDecorationType; - public readonly onDidChangeCodeLenses: Event = this._onDidChangeCodeLenses.event; +export class RMarkdownCodeLensProvider implements vscode.CodeLensProvider { + private codeLenses: vscode.CodeLens[] = []; + private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly decoration: vscode.TextEditorDecorationType; + public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; constructor() { - this.decoration = window.createTextEditorDecorationType({ + this.decoration = vscode.window.createTextEditorDecorationType({ isWholeLine: true, backgroundColor: config().get('rmarkdown.chunkBackgroundColor'), }); } - public provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { + public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.CodeLens[] | Thenable { this.codeLenses = []; const chunks = getChunks(document); - const chunkRanges: Range[] = []; + const chunkRanges: vscode.Range[] = []; const rmdCodeLensCommands: string[] = config().get('rmarkdown.codeLensCommands'); // Iterate through all code chunks for getting chunk information for both CodeLens and chunk background color (set by `editor.setDecorations`) @@ -74,61 +74,61 @@ export class RMarkdownCodeLensProvider implements CodeLensProvider { break; } this.codeLenses.push( - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Run Chunk', tooltip: 'Run current chunk', command: 'r.runCurrentChunk', arguments: [chunks, line] }), - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Run Above', tooltip: 'Run all chunks above', command: 'r.runAboveChunks', arguments: [chunks, line] }), - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Run Current & Below', tooltip: 'Run current and all chunks below', command: 'r.runCurrentAndBelowChunks', arguments: [chunks, line] }), - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Run Below', tooltip: 'Run all chunks below', command: 'r.runBelowChunks', arguments: [chunks, line] }), - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Run Previous', tooltip: 'Run previous chunk', command: 'r.runPreviousChunk', arguments: [chunks, line] }), - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Run Next', tooltip: 'Run next chunk', command: 'r.runNextChunk', arguments: [chunks, line] }), - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Run All', tooltip: 'Run all chunks', command: 'r.runAllChunks', arguments: [chunks] }), - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Go Previous', tooltip: 'Go to previous chunk', command: 'r.goToPreviousChunk', arguments: [chunks, line] }), - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Go Next', tooltip: 'Go to next chunk', command: 'r.goToNextChunk', arguments: [chunks, line] }), - new CodeLens(chunkRange, { + new vscode.CodeLens(chunkRange, { title: 'Select Chunk', tooltip: 'Select current chunk', command: 'r.selectCurrentChunk', @@ -138,7 +138,7 @@ export class RMarkdownCodeLensProvider implements CodeLensProvider { } } - for (const editor of window.visibleTextEditors) { + for (const editor of vscode.window.visibleTextEditors) { if (editor.document.uri.toString() === document.uri.toString()) { editor.setDecorations(this.decoration, chunkRanges); } @@ -154,7 +154,7 @@ export class RMarkdownCodeLensProvider implements CodeLensProvider { return sorted; }); } - public resolveCodeLens(codeLens: CodeLens): CodeLens { + public resolveCodeLens(codeLens: vscode.CodeLens): vscode.CodeLens { return codeLens; } } @@ -166,12 +166,12 @@ interface RMarkdownChunk { language: string; options: string; eval: boolean; - chunkRange: Range; - codeRange: Range; + chunkRange: vscode.Range; + codeRange: vscode.Range; } // Scan document and return chunk info (e.g. ID, chunk range) from all chunks -function getChunks(document: TextDocument): RMarkdownChunk[] { +function getChunks(document: vscode.TextDocument): RMarkdownChunk[] { const lines = document.getText().split(/\r?\n/); const chunks: RMarkdownChunk[] = []; @@ -197,13 +197,13 @@ function getChunks(document: TextDocument): RMarkdownChunk[] { if (isChunkEndLine(lines[line], isRDoc)) { chunkEndLine = line; - const chunkRange = new Range( - new Position(chunkStartLine, 0), - new Position(line, lines[line].length) + const chunkRange = new vscode.Range( + new vscode.Position(chunkStartLine, 0), + new vscode.Position(line, lines[line].length) ); - const codeRange = new Range( - new Position(chunkStartLine + 1, 0), - new Position(line - 1, lines[line - 1].length) + const codeRange = new vscode.Range( + new vscode.Position(chunkStartLine + 1, 0), + new vscode.Position(line - 1, lines[line - 1].length) ); chunks.push({ @@ -226,13 +226,13 @@ function getChunks(document: TextDocument): RMarkdownChunk[] { } function getCurrentChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk { - const lines = window.activeTextEditor.document.getText().split(/\r?\n/); + const lines = vscode.window.activeTextEditor.document.getText().split(/\r?\n/); let chunkStartLineAtOrAbove = line; // `- 1` to cover edge case when cursor is at 'chunk end line' let chunkEndLineAbove = line - 1; - const isRDoc = isRDocument(window.activeTextEditor.document); + const isRDoc = isRDocument(vscode.window.activeTextEditor.document); while (chunkStartLineAtOrAbove >= 0 && !isChunkStartLine(lines[chunkStartLineAtOrAbove], isRDoc)) { chunkStartLineAtOrAbove--; @@ -291,10 +291,10 @@ function getNextChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk { // Helpers function _getChunks() { - return getChunks(window.activeTextEditor.document); + return getChunks(vscode.window.activeTextEditor.document); } function _getStartLine() { - return window.activeTextEditor.selection.start.line; + return vscode.window.activeTextEditor.selection.start.line; } export async function runCurrentChunk(chunks: RMarkdownChunk[] = _getChunks(), @@ -331,7 +331,7 @@ export async function runAboveChunks(chunks: RMarkdownChunk[] = _getChunks(), const firstChunkId = 1; const previousChunkId = previousChunk.id; - const codeRanges: Range[] = []; + const codeRanges: vscode.Range[] = []; if (previousChunk !== currentChunk) { for (let i = firstChunkId; i <= previousChunkId; i++) { @@ -352,7 +352,7 @@ export async function runBelowChunks(chunks: RMarkdownChunk[] = _getChunks(), const nextChunkId = nextChunk.id; const lastChunkId = chunks.length; - const codeRanges: Range[] = []; + const codeRanges: vscode.Range[] = []; if (nextChunk !== currentChunk) { for (let i = nextChunkId; i <= lastChunkId; i++) { const chunk = chunks.find(e => e.id === i); @@ -370,7 +370,7 @@ export async function runCurrentAndBelowChunks(chunks: RMarkdownChunk[] = _getCh const currentChunkId = currentChunk.id; const lastChunkId = chunks.length; - const codeRanges: Range[] = []; + const codeRanges: vscode.Range[] = []; for (let i = currentChunkId; i <= lastChunkId; i++) { const chunk = chunks.find(e => e.id === i); @@ -384,7 +384,7 @@ export async function runAllChunks(chunks: RMarkdownChunk[] = _getChunks()): Pro const firstChunkId = 1; const lastChunkId = chunks.length; - const codeRanges: Range[] = []; + const codeRanges: vscode.Range[] = []; for (let i = firstChunkId; i <= lastChunkId; i++) { const chunk = chunks.find(e => e.id === i); @@ -398,8 +398,8 @@ export async function runAllChunks(chunks: RMarkdownChunk[] = _getChunks()): Pro async function goToChunk(chunk: RMarkdownChunk) { // Move cursor 1 line below 'chunk start line' const line = chunk.startLine + 1; - window.activeTextEditor.selection = new Selection(line, 0, line, 0); - await commands.executeCommand('revealLine', { lineNumber: line, at: 'center'}); + vscode.window.activeTextEditor.selection = new vscode.Selection(line, 0, line, 0); + await vscode.commands.executeCommand('revealLine', { lineNumber: line, at: 'center'}); } export function goToPreviousChunk(chunks: RMarkdownChunk[] = _getChunks(), @@ -416,17 +416,17 @@ export function goToNextChunk(chunks: RMarkdownChunk[] = _getChunks(), export function selectCurrentChunk(chunks: RMarkdownChunk[] = _getChunks(), line: number = _getStartLine()): void { - const editor = window.activeTextEditor; + const editor = vscode.window.activeTextEditor; const currentChunk = getCurrentChunk__CursorWithinChunk(chunks, line); const lines = editor.document.getText().split(/\r?\n/); - editor.selection = new Selection( + editor.selection = new vscode.Selection( currentChunk.startLine, 0, currentChunk.endLine, lines[currentChunk.endLine].length ); } -export class RMarkdownCompletionItemProvider implements CompletionItemProvider { +export class RMarkdownCompletionItemProvider implements vscode.CompletionItemProvider { // obtained from R code // paste0("[", paste0(paste0("'", names(knitr:: opts_chunk$merge(NULL)), "'"), collapse = ", "), "]") @@ -440,17 +440,17 @@ export class RMarkdownCompletionItemProvider implements CompletionItemProvider { 'external', 'sanitize', 'interval', 'aniopts', 'warning', 'error', 'message', 'render', 'ref.label', 'child', 'engine', 'split', 'include', 'purl']; - public readonly chunkOptionCompletionItems: CompletionItem[]; + public readonly chunkOptionCompletionItems: vscode.CompletionItem[]; constructor() { this.chunkOptionCompletionItems = this.chunkOptions.map((x: string) => { - const item = new CompletionItem(`${x}`); + const item = new vscode.CompletionItem(`${x}`); item.insertText = `${x}=`; return item; }); } - public provideCompletionItems(document: TextDocument, position: Position): CompletionItem[] { + public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem[] { const line = document.lineAt(position).text; if (isChunkStartLine(line, false) && getChunkLanguage(line) === 'r') { return this.chunkOptionCompletionItems; diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts new file mode 100644 index 000000000..39f906546 --- /dev/null +++ b/src/rmarkdown/knit.ts @@ -0,0 +1,253 @@ +import * as util from '../util'; +import * as vscode from 'vscode'; +import * as fs from 'fs-extra'; +import path = require('path'); +import yaml = require('js-yaml'); + +import { RMarkdownManager, KnitWorkingDirectory } from './manager'; +import { runTextInTerm } from '../rTerminal'; +import { rmdPreviewManager } from '../extension'; + +export let knitDir: KnitWorkingDirectory = util.config().get('rmarkdown.knit.defaults.knitWorkingDirectory') ?? undefined; + +interface IKnitQuickPickItem { + label: string, + description: string, + detail: string, + value: KnitWorkingDirectory +} + +interface IYamlFrontmatter { + title?: string, + author?: string, + knit?: string, + site?: string, + [key: string]: unknown +} + +export class RMarkdownKnitManager extends RMarkdownManager { + private async renderDocument(rPath: string, docPath: string, docName: string, yamlParams: IYamlFrontmatter, outputFormat?: string) { + const openOutfile: boolean = util.config().get('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 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}', ` + + `${knitCommand}, ` + + `'${lim}',` + + `sep='')"` + ); + + const callback = (dat: string) => { + const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); + if (outputUrl) { + if (openOutfile) { + const outFile = vscode.Uri.file(outputUrl); + if (fs.existsSync(outFile.fsPath)) { + void vscode.commands.executeCommand('vscode.open', outFile); + } else { + void vscode.window.showWarningMessage(`Could not find the output file at path: "${outFile.fsPath}"`); + } + } + return true; + } else { + return false; + } + }; + + if (util.config().get('rmarkdown.knit.focusOutputChannel')) { + this.rMarkdownOutput.show(true); + } + + return await this.knitWithProgress( + { + fileName: docName, + filePath: rPath, + cmd: cmd, + rCmd: knitCommand, + rOutputFormat: outputFormat, + callback: callback + } + ); + + } + + private getYamlFrontmatter(docPath: string): IYamlFrontmatter { + const parseData = fs.readFileSync(docPath, 'utf8'); + const yamlDat = /(?<=(---)).*(?=(---))/gs.exec( + parseData + ); + + let paramObj = {}; + if (yamlDat) { + try { + paramObj = yaml.load( + yamlDat[0] + ); + } catch (e) { + console.error(`Could not parse YAML frontmatter for "${docPath}". Error: ${String(e)}`); + } + } + + return paramObj; + } + + private async getKnitCommand(yamlParams: IYamlFrontmatter, docPath: string, outputFormat: string): Promise { + let knitCommand: string; + + if (!yamlParams?.['site']) { + yamlParams['site'] = await this.findSiteParam(); + } + + // precedence: + // knit > site > none + if (yamlParams?.['knit']) { + const knitParam = yamlParams['knit']; + knitCommand = outputFormat ? + `${knitParam}(${docPath}, output_format = '${outputFormat}')`: + `${knitParam}(${docPath})`; + } else if (!this.isREADME(docPath) && yamlParams?.['site']) { + knitCommand = outputFormat ? + `rmarkdown::render_site(${docPath}, output_format = '${outputFormat}')` : + `rmarkdown::render_site(${docPath})`; + } else { + knitCommand = outputFormat ? + `rmarkdown::render(${docPath}, output_format = '${outputFormat}')` : + `rmarkdown::render(${docPath})`; + } + + return knitCommand.replace(/['"]/g, '\\"'); + } + + // check if the workspace of the document is a R Markdown site. + // the definition of what constitutes an R Markdown site differs + // depending on the type of R Markdown site (i.e., "simple" vs. blogdown sites) + private async findSiteParam(): Promise { + const rootFolder = vscode.workspace.workspaceFolders[0].uri.fsPath; + const wad = vscode.window.activeTextEditor.document.uri.fsPath; + const indexFile = (await vscode.workspace.findFiles(new vscode.RelativePattern(rootFolder, 'index.{Rmd,rmd, md}'), null, 1))?.[0]; + const siteRoot = path.join(path.dirname(wad), '_site.yml'); + + // 'Simple' R Markdown websites require all docs to be in the root folder + if (fs.existsSync(siteRoot)) { + return 'rmarkdown::render_site'; + // Other generators may allow for docs in subdirs + } else if (indexFile) { + const indexData = this.getYamlFrontmatter(indexFile.fsPath); + if (indexData?.['site']) { + return indexData['site']; + } + } + + return undefined; + } + + // readme files should not be knitted via render_site + private isREADME(docPath: string) { + return !!path.basename(docPath).includes('README'); + } + + // alters the working directory for evaluating chunks + public setKnitDir(): void { + const currentDocumentWorkspacePath: string = vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor?.document?.uri)?.uri?.fsPath; + const currentDocumentFolderPath: string = path.dirname(vscode.window?.activeTextEditor.document?.uri?.fsPath); + const items: IKnitQuickPickItem[] = []; + + if (currentDocumentWorkspacePath) { + items.push( + { + label: (knitDir === KnitWorkingDirectory.workspaceRoot ? '$(check)' : '') + KnitWorkingDirectory.workspaceRoot, + value: KnitWorkingDirectory.workspaceRoot, + detail: 'Use the workspace root as the knit working directory', + description: currentDocumentWorkspacePath ?? currentDocumentFolderPath ?? 'No available workspace' + } + ); + } + + if (currentDocumentFolderPath && currentDocumentFolderPath !== '.') { + items.push( + { + label: (knitDir === KnitWorkingDirectory.documentDirectory ? '$(check)' : '') + KnitWorkingDirectory.documentDirectory, + value: KnitWorkingDirectory.documentDirectory, + detail: 'Use the document\'s directory as the knit working directory', + description: currentDocumentFolderPath ?? 'No folder available' + + } + ); + } + + if (items.length > 0) { + void vscode.window.showQuickPick( + items, + { + title: 'Set knit working directory', + canPickMany: false + } + ).then(async choice => { + if (choice?.value && knitDir !== choice.value) { + knitDir = choice.value; + await rmdPreviewManager.updatePreview(); + } + }); + } else { + void vscode.window.showInformationMessage('Cannot set knit directory for untitled documents.'); + } + + } + + public async knitRmd(echo: boolean, outputFormat?: string): Promise { + const wad: vscode.TextDocument = vscode.window.activeTextEditor.document; + + // handle untitled rmd + if (vscode.window.activeTextEditor.document.isUntitled) { + void vscode.window.showWarningMessage('Cannot knit an untitled file. Please save the document.'); + await vscode.commands.executeCommand('workbench.action.files.save').then(() => { + if (!vscode.window.activeTextEditor.document.isUntitled) { + void this.knitRmd(echo, outputFormat); + } + }); + return; + } + + const isSaved = await util.saveDocument(wad); + if (isSaved) { + let rPath = util.ToRStringLiteral(wad.fileName, '"'); + let encodingParam = util.config().get('source.encoding'); + encodingParam = `encoding = "${encodingParam}"`; + rPath = [rPath, encodingParam].join(', '); + if (echo) { + rPath = [rPath, 'echo = TRUE'].join(', '); + } + + // allow users to opt out of background process + if (util.config().get('rmarkdown.knit.useBackgroundProcess')) { + const busyPath = wad.uri.fsPath + outputFormat; + if (this.busyUriStore.has(busyPath)) { + return; + } else { + this.busyUriStore.add(busyPath); + await this.renderDocument( + rPath, + wad.uri.fsPath, + path.basename(wad.uri.fsPath), + this.getYamlFrontmatter(wad.uri.fsPath), + outputFormat + ); + this.busyUriStore.delete(busyPath); + } + } else { + if (outputFormat === undefined) { + void runTextInTerm(`rmarkdown::render(${rPath})`); + } else { + void runTextInTerm(`rmarkdown::render(${rPath}, "${outputFormat}")`); + } + } + } + } +} diff --git a/src/rmarkdown/manager.ts b/src/rmarkdown/manager.ts new file mode 100644 index 000000000..7de093958 --- /dev/null +++ b/src/rmarkdown/manager.ts @@ -0,0 +1,148 @@ +import * as util from '../util'; +import * as vscode from 'vscode'; +import * as cp from 'child_process'; +import path = require('path'); + +export enum KnitWorkingDirectory { + documentDirectory = 'document directory', + workspaceRoot = 'workspace root', +} + +interface IKnitArgs { + filePath: string; + fileName: string; + cmd: string; + rCmd?: string; + rOutputFormat?: string; + callback: (...args: unknown[]) => boolean; + 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; + // uri that are in the process of knitting + // so that we can't spam the knit/preview button + protected busyUriStore: Set = new Set(); + + protected getKnitDir(knitDir: string, docPath?: string): string { + const currentDocumentWorkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(docPath) ?? vscode.window.activeTextEditor?.document?.uri)?.uri?.fsPath ?? undefined; + switch (knitDir) { + // the directory containing the R Markdown document + case KnitWorkingDirectory.documentDirectory: { + return path.dirname(docPath).replace(/\\/g, '/').replace(/['"]/g, '\\"'); + } + // the root of the current workspace + case KnitWorkingDirectory.workspaceRoot: { + return currentDocumentWorkspace.replace(/\\/g, '/').replace(/['"]/g, '\\"'); + } + // the working directory of the attached terminal, NYI + // case 'current directory': { + // return NULL + // } + default: return undefined; + } + } + + protected async knitDocument(args: IKnitArgs, token?: vscode.CancellationToken, progress?: vscode.Progress): Promise { + // vscode.Progress auto-increments progress, so we use this + // variable to set progress to a specific number + let currentProgress = 0; + return await new Promise( + (resolve, reject) => { + const cmd = args.cmd; + const fileName = args.fileName; + let childProcess: cp.ChildProcessWithoutNullStreams; + + try { + childProcess = cp.exec(cmd); + progress.report({ + increment: 0, + message: '0%' + }); + } catch (e: unknown) { + console.warn(`[VSC-R] error: ${e as string}`); + reject({ cp: childProcess, wasCancelled: false }); + } + + this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process started`); + + if (args.rCmd) { + this.rMarkdownOutput.appendLine(`==> ${args.rCmd}`); + } + + childProcess.stdout.on('data', + (data: Buffer) => { + const dat = data.toString('utf8'); + 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); + progress.report( + { + increment: perc - currentProgress, + message: `${perc}%` + } + ); + currentProgress = perc; + } + } + if (token?.isCancellationRequested) { + resolve(childProcess); + } else { + if (args.callback(dat, childProcess)) { + resolve(childProcess); + } + } + } + ); + + childProcess.stderr.on('data', (data: Buffer) => { + const dat = data.toString('utf8'); + this.rMarkdownOutput.appendLine(dat); + }); + + childProcess.on('exit', (code, signal) => { + this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process exited ` + + (signal ? `from signal '${signal}'` : `with exit code ${code}`)); + if (code !== 0) { + reject({ cp: childProcess, wasCancelled: false }); + } + }); + + token?.onCancellationRequested(() => { + reject({ cp: childProcess, wasCancelled: true }); + }); + } + ); + } + + protected async knitWithProgress(args: IKnitArgs): Promise { + let childProcess: cp.ChildProcessWithoutNullStreams = undefined; + await util.doWithProgress(async (token: vscode.CancellationToken, progress: vscode.Progress) => { + childProcess = await this.knitDocument(args, token, progress) as cp.ChildProcessWithoutNullStreams; + }, + vscode.ProgressLocation.Notification, + `Knitting ${args.fileName} ${args.rOutputFormat ? 'to ' + args.rOutputFormat : ''} `, + true + ).catch((rejection: IKnitRejection) => { + if (!rejection.wasCancelled) { + void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); + 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'); + }); + return childProcess; + } +} diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index f721667bf..571c4f1f9 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -8,8 +8,10 @@ import path = require('path'); import crypto = require('crypto'); -import { config, doWithProgress, getRpath, readContent, setContext, escapeHtml, UriIcon } from '../util'; +import { config, readContent, setContext, escapeHtml, UriIcon, saveDocument, getRpath } from '../util'; import { extensionContext, tmpDir } from '../extension'; +import { knitDir } from './knit'; +import { IKnitRejection, RMarkdownManager } from './manager'; class RMarkdownPreview extends vscode.Disposable { title: string; @@ -164,24 +166,19 @@ class RMarkdownPreviewStore extends vscode.Disposable { } } -export class RMarkdownPreviewManager { - private rPath: string; - private rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); - +export class RMarkdownPreviewManager extends RMarkdownManager { // the currently selected RMarkdown preview - private activePreview: { filePath: string, preview: RMarkdownPreview } = { filePath: null, preview: null }; + private activePreview: { filePath: string, preview: RMarkdownPreview, title: string } = { filePath: null, preview: null, title: null }; // store of all open RMarkdown previews private previewStore: RMarkdownPreviewStore = new RMarkdownPreviewStore; - // uri that are in the process of knitting - // so that we can't spam the preview button - private busyUriStore: Set = new Set(); private useCodeTheme = true; - public async init(): Promise { - this.rPath = await getRpath(true); + constructor() { + super(); extensionContext.subscriptions.push(this.previewStore); } + public async previewRmd(viewer: vscode.ViewColumn, uri?: vscode.Uri): Promise { const filePath = uri ? uri.fsPath : vscode.window.activeTextEditor.document.uri.fsPath; const fileName = path.basename(filePath); @@ -198,16 +195,21 @@ export class RMarkdownPreviewManager { return; } - // don't knit if the current uri is already being knit - if (this.busyUriStore.has(filePath)) { - return; - } else if (this.previewStore.has(filePath)) { - this.previewStore.get(filePath)?.panel.reveal(); - } else { - this.busyUriStore.add(filePath); - await vscode.commands.executeCommand('workbench.action.files.save'); - await this.knitWithProgress(filePath, fileName, viewer, currentViewColumn); - this.busyUriStore.delete(filePath); + const isSaved = uri ? + true : + await saveDocument(vscode.window.activeTextEditor.document); + + if (isSaved) { + // don't knit if the current uri is already being knit + if (this.busyUriStore.has(filePath)) { + return; + } else if (this.previewStore.has(filePath)) { + this.previewStore.get(filePath)?.panel.reveal(); + } else { + this.busyUriStore.add(filePath); + await this.previewDocument(filePath, fileName, viewer, currentViewColumn); + this.busyUriStore.delete(filePath); + } } } @@ -263,117 +265,81 @@ export class RMarkdownPreviewManager { public async updatePreview(preview?: RMarkdownPreview): Promise { const toUpdate = preview ?? this.activePreview?.preview; const previewUri = this.previewStore?.getFilePath(toUpdate); - toUpdate.cp?.kill('SIGKILL'); + toUpdate?.cp?.kill('SIGKILL'); - const childProcess: cp.ChildProcessWithoutNullStreams | void = await this.knitWithProgress(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); - }); + if (toUpdate) { + const childProcess: cp.ChildProcessWithoutNullStreams | 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); + }); + + if (childProcess) { + toUpdate.cp = childProcess; + } - if (childProcess) { - toUpdate.cp = childProcess; + this.refreshPanel(toUpdate); } - this.refreshPanel(toUpdate); } - private async knitWithProgress(filePath: string, fileName: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn) { - let childProcess:cp.ChildProcessWithoutNullStreams = undefined; - await doWithProgress(async (token: vscode.CancellationToken) => { - childProcess = await this.knitDocument(filePath, fileName, token, viewer, currentViewColumn); - }, - vscode.ProgressLocation.Notification, - `Knitting ${fileName}...`, - true - ).catch((rejection: { - cp: cp.ChildProcessWithoutNullStreams, - wasCancelled?: boolean - }) => { - if (!rejection.wasCancelled) { - void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); - this.rMarkdownOutput.show(true); + private async previewDocument(filePath: string, fileName?: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn) { + const knitWorkingDir = this.getKnitDir(knitDir, filePath); + const knitWorkingDirText = knitWorkingDir ? `'${knitWorkingDir}'` : `NULL`; + this.rPath = await getRpath(true); + + 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(` + + `'${filePath.replace(/\\/g, '/')}',` + + `output_format = rmarkdown::html_document(),` + + `output_file = '${outputFile.replace(/\\/g, '/')}',` + + `intermediates_dir = '${tmpDir().replace(/\\/g, '/')}'), '${lim}',` + + `sep='')"` + ); + + const callback = (dat: string, childProcess: cp.ChildProcessWithoutNullStreams) => { + const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); + if (outputUrl) { + if (viewer !== undefined) { + const autoRefresh = config().get('rmarkdown.preview.autoRefresh'); + void this.openPreview( + vscode.Uri.parse(outputUrl), + filePath, + fileName, + childProcess, + viewer, + currentViewColumn, + autoRefresh + ); + } + return true; } - // this can occur when a successfuly knitted document is later altered (while still being previewed) - // and subsequently fails to knit + return false; + }; + + const onRejected = (filePath: string, rejection: IKnitRejection) => { if (this.previewStore.has(filePath)) { this.previewStore.delete(filePath); } else { rejection.cp.kill('SIGKILL'); } - }); - return childProcess; - } + }; - private async knitDocument(filePath: string, fileName: string, token?: vscode.CancellationToken, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn) { - return await new Promise((resolve, reject) => { - 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 ` + - `"cat('${lim}', rmarkdown::render(` + - `'${filePath.replace(/\\/g, '/')}',` + - `output_format = rmarkdown::html_document(),` + - `output_file = '${outputFile.replace(/\\/g, '/')}',` + - `intermediates_dir = '${tmpDir().replace(/\\/g, '/')}'), '${lim}',` + - `sep='')"` - ); - - let childProcess: cp.ChildProcessWithoutNullStreams; - try { - childProcess = cp.exec(cmd); - } catch (e: unknown) { - console.warn(`[VSC-R] error: ${e as string}`); - reject({ cp: childProcess, wasCancelled: false }); + return await this.knitWithProgress( + { + fileName: fileName, + filePath: filePath, + cmd: cmd, + rOutputFormat: 'html preview', + callback: callback, + onRejection: onRejected } - - this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process started`); - - childProcess.stdout.on('data', - (data: Buffer) => { - const dat = data.toString('utf8'); - this.rMarkdownOutput.appendLine(dat); - if (token?.isCancellationRequested) { - resolve(childProcess); - } else { - const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); - if (outputUrl) { - if (viewer !== undefined) { - const autoRefresh = config().get('rmarkdown.preview.autoRefresh'); - void this.openPreview( - vscode.Uri.parse(outputUrl), - filePath, - fileName, - childProcess, - viewer, - currentViewColumn, - autoRefresh - ); - } - resolve(childProcess); - } - } - } - ); - - childProcess.stderr.on('data', (data: Buffer) => { - const dat = data.toString('utf8'); - this.rMarkdownOutput.appendLine(dat); - }); - - childProcess.on('exit', (code, signal) => { - this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process exited ` + - (signal ? `from signal '${signal}'` : `with exit code ${code}`)); - if (code !== 0) { - reject({ cp: childProcess, wasCancelled: false }); - } - }); - - token?.onCancellationRequested(() => { - reject({ cp: childProcess, wasCancelled: true }); - }); - }); + ); } private openPreview(outputUri: vscode.Uri, filePath: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn, autoRefresh: boolean): void { @@ -414,7 +380,7 @@ export class RMarkdownPreviewManager { // state change panel.onDidDispose(() => { // clear values - this.activePreview = this.activePreview?.preview === preview ? { filePath: null, preview: null } : this.activePreview; + this.activePreview = this.activePreview?.preview === preview ? { filePath: null, preview: null, title: null } : this.activePreview; void setContext('r.rmarkdown.preview.active', false); this.previewStore.delete(filePath); }); @@ -424,6 +390,7 @@ export class RMarkdownPreviewManager { if (webviewPanel.active) { this.activePreview.preview = preview; this.activePreview.filePath = filePath; + this.activePreview.title = title; void setContext('r.rmarkdown.preview.autoRefresh', preview.autoRefresh); } }); diff --git a/src/util.ts b/src/util.ts index c392a8953..cffeeb3a8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -231,7 +231,7 @@ export async function executeAsTask(name: string, command: string, args?: string // executes a callback and shows a 'busy' progress bar during the execution // synchronous callbacks are converted to async to properly render the progress bar // default location is in the help pages tree view -export async function doWithProgress(cb: (token?: vscode.CancellationToken) => T | Promise, location: string | vscode.ProgressLocation = 'rHelpPages', title?: string, cancellable?: boolean): Promise { +export async function doWithProgress(cb: (token?: vscode.CancellationToken, progress?: vscode.Progress) => T | Promise, location: string | vscode.ProgressLocation = 'rHelpPages', title?: string, cancellable?: boolean): Promise { const location2 = (typeof location === 'string' ? { viewId: location } : location); const options: vscode.ProgressOptions = { location: location2, @@ -239,9 +239,9 @@ export async function doWithProgress(cb: (token?: vscode.CancellationToken) = title: title }; let ret: T; - await vscode.window.withProgress(options, async (_progress, token) => { + await vscode.window.withProgress(options, async (progress, token) => { const retPromise = new Promise((resolve) => setTimeout(() => { - const ret = cb(token); + const ret = cb(token, progress); resolve(ret); })); ret = await retPromise; @@ -249,7 +249,6 @@ export async function doWithProgress(cb: (token?: vscode.CancellationToken) = return ret; } - // get the URL of a CRAN website // argument path is optional and should be relative to the cran root // currently the CRAN root url is hardcoded, this could be replaced by reading diff --git a/yarn.lock b/yarn.lock index 687d04424..85c3c1b0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -194,6 +194,11 @@ dependencies: highlight.js "*" +"@types/js-yaml@^4.0.2": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.3.tgz#9f33cd6fbf0d5ec575dc8c8fc69c7fec1b4eb200" + integrity sha512-5t9BhoORasuF5uCPr+d5/hdB++zRFUTMIZOzbNkr+jZh3yQht4HYbRDyj9fY8n2TZT30iW9huzav73x4NikqWg== + "@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.6": version "7.0.7" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" @@ -1736,6 +1741,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsdoc-type-pratt-parser@1.0.0-alpha.23: version "1.0.0-alpha.23" resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-1.0.0-alpha.23.tgz#01c232d92b99b7e7ef52235ab8c9115137426639" From df8e1478a4840622734bcae2767f3535706a3e5b Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 6 Sep 2021 09:52:37 +0000 Subject: [PATCH 19/60] (Refactoring) Simplify RMD child process disposal (#773) - Contributes a helper function for creating disposables from objects - Simplifies disposal of child processes - Do not print lines to output when the process has already been terminated - use childProcess.spawn instead of childProcess.exec, to allow for faster process termination --- src/rmarkdown/knit.ts | 34 +++++++++---------- src/rmarkdown/manager.ts | 70 +++++++++++++++++++++++++++++----------- src/rmarkdown/preview.ts | 29 +++++++---------- src/util.ts | 17 ++++++++++ 4 files changed, 97 insertions(+), 53 deletions(-) diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index 39f906546..25a5afbd0 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -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'; @@ -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 { const openOutfile: boolean = util.config().get('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'); @@ -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, @@ -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. @@ -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('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 @@ -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), @@ -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}')`); } } } diff --git a/src/rmarkdown/manager.ts b/src/rmarkdown/manager.ts index 7de093958..0ef0a45a1 100644 --- a/src/rmarkdown/manager.ts +++ b/src/rmarkdown/manager.ts @@ -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; @@ -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; @@ -51,18 +53,44 @@ export abstract class RMarkdownManager { } } - protected async knitDocument(args: IKnitArgs, token?: vscode.CancellationToken, progress?: vscode.Progress): Promise { + protected async knitDocument(args: IKnitArgs, token?: vscode.CancellationToken, progress?: vscode.Progress): Promise { // vscode.Progress auto-increments progress, so we use this // variable to set progress to a specific number let currentProgress = 0; - return await new Promise( + let printOutput = true; + + return await new Promise( (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%' @@ -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); @@ -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) => { @@ -126,10 +160,10 @@ export abstract class RMarkdownManager { ); } - protected async knitWithProgress(args: IKnitArgs): Promise { - let childProcess: cp.ChildProcessWithoutNullStreams = undefined; + protected async knitWithProgress(args: IKnitArgs): Promise { + let childProcess: DisposableProcess = undefined; await util.doWithProgress(async (token: vscode.CancellationToken, progress: vscode.Progress) => { - 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 : ''} `, @@ -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; } diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 571c4f1f9..4f72ecd8b 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -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'; @@ -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; @@ -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(() => { @@ -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); @@ -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 { 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) { @@ -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'); } }; @@ -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', diff --git a/src/util.ts b/src/util.ts index cffeeb3a8..9b14f13e5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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(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; +} From 3e0619ce454f0fdc286459b7c7f2af1a7d2ae7b2 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 9 Sep 2021 19:16:59 +0800 Subject: [PATCH 20/60] Add object length limit (#778) * Add object length limit * Update inspect_env * Preserve the order of names in completion --- R/vsc.R | 27 +++++++++++++++---------- package.json | 5 +++++ src/completions.ts | 50 ++++++++++++++++++++++------------------------ 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/R/vsc.R b/R/vsc.R index 43e0cff83..2e0455b52 100644 --- a/R/vsc.R +++ b/R/vsc.R @@ -20,6 +20,7 @@ load_settings <- function() { vsc.show_object_size = workspaceViewer$showObjectSize, vsc.rstudioapi = session$emulateRStudioAPI, vsc.str.max.level = session$levelOfObjectDetail, + vsc.object_length_limit = session$objectLengthLimit, vsc.globalenv = session$watchGlobalEnvironment, vsc.plot = session$viewers$viewColumn$plot, vsc.browser = session$viewers$viewColumn$browser, @@ -133,6 +134,8 @@ inspect_env <- function(env, cache) { is_promise <- rlang::env_binding_are_lazy(env, all_names) is_active <- rlang::env_binding_are_active(env, all_names) show_object_size <- getOption("vsc.show_object_size", FALSE) + object_length_limit <- getOption("vsc.object_length_limit", 2000) + str_max_level <- getOption("vsc.str.max.level", 0) objs <- lapply(all_names, function(name) { if (is_promise[[name]]) { info <- list( @@ -154,8 +157,7 @@ inspect_env <- function(env, cache) { info <- list( class = class(obj), type = scalar(typeof(obj)), - length = scalar(length(obj)), - str = scalar(trimws(capture_str(obj))) + length = scalar(length(obj)) ) if (show_object_size) { @@ -171,14 +173,19 @@ inspect_env <- function(env, cache) { info$size <- scalar(cobj$size) } - obj_names <- if (is.object(obj)) { - .DollarNames(obj, pattern = "") - } else if (is.recursive(obj)) { - names(obj) - } else NULL - - if (length(obj_names)) { - info$names <- obj_names + if (length(obj) > object_length_limit) { + info$str <- scalar(trimws(capture_str(obj, 0))) + } else { + info$str <- scalar(trimws(capture_str(obj, str_max_level))) + obj_names <- if (is.object(obj)) { + .DollarNames(obj, pattern = "") + } else if (is.recursive(obj)) { + names(obj) + } else NULL + + if (length(obj_names)) { + info$names <- obj_names + } } if (isS4(obj)) { diff --git a/package.json b/package.json index 483f0c24e..a365b40dc 100644 --- a/package.json +++ b/package.json @@ -1398,6 +1398,11 @@ "default": true, "markdownDescription": "Watch the global environment to provide hover, autocompletions, and workspace viewer information. Changes the option `vsc.globalenv` in R. Requires `#r.sessionWatcher#` to be set to `true`." }, + "r.session.objectLengthLimit": { + "type": "integer", + "default": 2000, + "markdownDescription": "The upper limit of object length to show object details in workspace viewer and provide session symbol completion. Decrease this value if you experience significant delay after executing R commands caused by large global objects with many elements. Changes the option `vsc.object_length_limit` in R. Requires `#r.sessionWatcher#` to be set to `true`." + }, "r.session.levelOfObjectDetail": { "type": "string", "markdownDescription": "How much of the object to show on hover, autocompletion, and in the workspace viewer? Changes the option `vsc.str.max.level` in R. Requires `#r.sessionWatcher#` to be set to `true`.", diff --git a/src/completions.ts b/src/completions.ts index 52b9c743e..34e92f2b4 100644 --- a/src/completions.ts +++ b/src/completions.ts @@ -96,7 +96,7 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider const trigger = completionContext.triggerCharacter; if (trigger === undefined) { - Object.keys(session.globalenv).map((key) => { + Object.keys(session.globalenv).forEach((key) => { const obj = session.globalenv[key]; const item = new vscode.CompletionItem( key, @@ -114,36 +114,44 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider const symbol = document.getText(symbolRange); const doc = new vscode.MarkdownString('Element of `' + symbol + '`'); const obj = session.globalenv[symbol]; - let elements: string[]; + let names: string[]; if (obj !== undefined) { if (completionContext.triggerCharacter === '$') { - elements = obj.names; + names = obj.names; } else if (completionContext.triggerCharacter === '@') { - elements = obj.slots; + names = obj.slots; } } - elements.map((key) => { - const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Field); - item.detail = '[session]'; - item.documentation = doc; - items.push(item); - }); + + if (names) { + items.push(...getCompletionItems(names, vscode.CompletionItemKind.Field, '[session]', doc)); + } } if (trigger === undefined || trigger === '[' || trigger === ',' || trigger === '"' || trigger === '\'') { - const bracketItems = getBracketCompletionItems(document, position, token); - items.push(...bracketItems); + items.push(...getBracketCompletionItems(document, position, token)); } if (trigger === undefined || trigger === '(' || trigger === ',') { - const pipelineItems = getPipelineCompletionItems(document, position, token); - items.push(...pipelineItems); + items.push(...getPipelineCompletionItems(document, position, token)); } return items; } } +function getCompletionItems(names: string[], kind: vscode.CompletionItemKind, detail: string, documentation: vscode.MarkdownString): vscode.CompletionItem[] { + const len = names.length.toString().length; + let index = 0; + return names.map((name) => { + const item = new vscode.CompletionItem(name, kind); + item.detail = detail; + item.documentation = documentation; + item.sortText = `0-${index.toString().padStart(len, '0')}`; + index++; + return item; + }); +} function getBracketCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) { const items: vscode.CompletionItem[] = []; @@ -181,12 +189,7 @@ function getBracketCompletionItems(document: vscode.TextDocument, position: vsco const obj = session.globalenv[symbol]; if (obj !== undefined && obj.names !== undefined) { const doc = new vscode.MarkdownString('Element of `' + symbol + '`'); - obj.names.map((name: string) => { - const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Field); - item.detail = '[session]'; - item.documentation = doc; - items.push(item); - }); + items.push(...getCompletionItems(obj.names, vscode.CompletionItemKind.Field, '[session]', doc)); } } return items; @@ -231,12 +234,7 @@ function getPipelineCompletionItems(document: vscode.TextDocument, position: vsc const obj = session.globalenv[symbol]; if (obj !== undefined && obj.names !== undefined) { const doc = new vscode.MarkdownString('Element of `' + symbol + '`'); - obj.names.map((name: string) => { - const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Field); - item.detail = '[session]'; - item.documentation = doc; - items.push(item); - }); + items.push(...getCompletionItems(obj.names, vscode.CompletionItemKind.Field, '[session]', doc)); } } return items; From 911249f44efcd376cb3b1785a16c05a08e2c7146 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 9 Sep 2021 22:02:41 +0800 Subject: [PATCH 21/60] Write NA as string (#780) --- R/vsc.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/vsc.R b/R/vsc.R index 2e0455b52..cc1728664 100644 --- a/R/vsc.R +++ b/R/vsc.R @@ -425,13 +425,13 @@ if (show_view) { if (is.data.frame(x) || is.matrix(x)) { data <- dataview_table(x) file <- tempfile(tmpdir = tempdir, fileext = ".json") - jsonlite::write_json(data, file, auto_unbox = TRUE) + jsonlite::write_json(data, file, na = "string", null = "null", auto_unbox = TRUE) request("dataview", source = "table", type = "json", title = title, file = file, viewer = viewer, uuid = uuid) } else if (is.list(x)) { tryCatch({ file <- tempfile(tmpdir = tempdir, fileext = ".json") - jsonlite::write_json(x, file, auto_unbox = TRUE) + jsonlite::write_json(x, file, na = "string", null = "null", auto_unbox = TRUE) request("dataview", source = "list", type = "json", title = title, file = file, viewer = viewer, uuid = uuid) }, error = function(e) { From 81cf926b77a91c860c4803ed351d4c73b3198da1 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Fri, 10 Sep 2021 08:43:52 +0000 Subject: [PATCH 22/60] Use R files for background process (#783) * Move R-related session files to `R/session` * Move `getAliases.R` to `R/help` * New file `helpServer.R` in `R/help` * New file `knit.R` in `R/rmarkdown` * New file `preview.R` in `R/rmarkdown` --- R/{ => help}/getAliases.R | 0 R/help/helpServer.R | 11 +++++++++++ R/rmarkdown/knit.R | 22 ++++++++++++++++++++++ R/rmarkdown/preview.R | 29 +++++++++++++++++++++++++++++ R/{ => session}/.Rprofile | 0 R/{ => session}/init.R | 0 R/{ => session}/rstudioapi.R | 0 R/{ => session}/rstudioapi_util.R | 0 R/{ => session}/vsc.R | 0 src/helpViewer/helpProvider.ts | 17 ++++++++++------- src/helpViewer/index.ts | 2 +- src/rTerminal.ts | 4 ++-- src/rmarkdown/knit.ts | 18 +++++++++--------- src/rmarkdown/manager.ts | 16 +++++++++++----- src/rmarkdown/preview.ts | 22 +++++++++++----------- src/session.ts | 2 +- 16 files changed, 107 insertions(+), 36 deletions(-) rename R/{ => help}/getAliases.R (100%) create mode 100644 R/help/helpServer.R create mode 100644 R/rmarkdown/knit.R create mode 100644 R/rmarkdown/preview.R rename R/{ => session}/.Rprofile (100%) rename R/{ => session}/init.R (100%) rename R/{ => session}/rstudioapi.R (100%) rename R/{ => session}/rstudioapi_util.R (100%) rename R/{ => session}/vsc.R (100%) diff --git a/R/getAliases.R b/R/help/getAliases.R similarity index 100% rename from R/getAliases.R rename to R/help/getAliases.R diff --git a/R/help/helpServer.R b/R/help/helpServer.R new file mode 100644 index 000000000..7b6d264b5 --- /dev/null +++ b/R/help/helpServer.R @@ -0,0 +1,11 @@ +# get values from extension-set env values +lim <- Sys.getenv("VSCR_LIM") + +cat( + lim, + tools::startDynamicHelp(), + lim, + sep = "" +) + +while (TRUE) Sys.sleep(1) diff --git a/R/rmarkdown/knit.R b/R/rmarkdown/knit.R new file mode 100644 index 000000000..895fa27b5 --- /dev/null +++ b/R/rmarkdown/knit.R @@ -0,0 +1,22 @@ +# requires rmarkdown package to run (and knitr) +if (!requireNamespace(rmarkdown, quietly = TRUE)) { + stop("Knitting requires the {rmarkdown} package.") +} + +# get values from extension-set env values + +knit_dir <- Sys.getenv("VSCR_KNIT_DIR") +lim <- Sys.getenv("VSCR_LIM") +knit_command <- Sys.getenv("VSCR_KNIT_COMMAND") + +# set the knitr chunk eval directory +# mainly affects source calls +knitr::opts_knit[["set"]](root.dir = knit_dir) + +# render and get file output location for use in extension +cat( + lim, + eval(parse(text = knit_command)), + lim, + sep = "" +) \ No newline at end of file diff --git a/R/rmarkdown/preview.R b/R/rmarkdown/preview.R new file mode 100644 index 000000000..ecf5e6cc4 --- /dev/null +++ b/R/rmarkdown/preview.R @@ -0,0 +1,29 @@ +# requires rmarkdown package to run (and knitr) +if (!requireNamespace(rmarkdown, quietly = TRUE)) { + stop("Previewing documents requires the {rmarkdown} package.") +} + +# get values from extension-set env values + +knit_dir <- Sys.getenv("VSCR_KNIT_DIR") +lim <- Sys.getenv("VSCR_LIM") +file_path <- Sys.getenv("VSCR_FILE_PATH") +output_file_loc <- Sys.getenv("VSCR_OUTPUT_FILE") +tmp_dir <- Sys.getenv("VSCR_TMP_DIR") + +# set the knitr chunk eval directory +# mainly affects source calls +knitr::opts_knit[["set"]](root.dir = knit_dir) + +# render and get file output location for use in extension +cat( + lim, + rmarkdown::render( + file_path, + output_format = rmarkdown::html_document(), + output_file = output_file_loc, + intermediates_dir = tmp_dir + ), + lim, + sep = "" +) \ No newline at end of file diff --git a/R/.Rprofile b/R/session/.Rprofile similarity index 100% rename from R/.Rprofile rename to R/session/.Rprofile diff --git a/R/init.R b/R/session/init.R similarity index 100% rename from R/init.R rename to R/session/init.R diff --git a/R/rstudioapi.R b/R/session/rstudioapi.R similarity index 100% rename from R/rstudioapi.R rename to R/session/rstudioapi.R diff --git a/R/rstudioapi_util.R b/R/session/rstudioapi_util.R similarity index 100% rename from R/rstudioapi_util.R rename to R/session/rstudioapi_util.R diff --git a/R/vsc.R b/R/session/vsc.R similarity index 100% rename from R/vsc.R rename to R/session/vsc.R diff --git a/src/helpViewer/helpProvider.ts b/src/helpViewer/helpProvider.ts index 510893e03..12f9888e3 100644 --- a/src/helpViewer/helpProvider.ts +++ b/src/helpViewer/helpProvider.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as rHelp from '.'; +import { extensionContext } from '../extension'; export interface RHelpProviderOptions { // path of the R executable @@ -39,12 +40,14 @@ export class HelpProvider { const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'ms'); // starts the background help server and waits forever to keep the R process running + const scriptPath = extensionContext.asAbsolutePath('R/help/helpServer.R'); const cmd = ( - `${this.rPath} --silent --slave --no-save --no-restore -e ` + - `"cat('${lim}', tools::startDynamicHelp(), '${lim}', sep=''); while(TRUE) Sys.sleep(1)" ` + `${this.rPath} --silent --slave --no-save --no-restore -f ` + + `${scriptPath}` ); const cpOptions = { - cwd: this.cwd + cwd: this.cwd, + env: { ...process.env, 'VSCR_LIM': lim } }; this.cp = cp.exec(cmd, cpOptions); @@ -53,7 +56,7 @@ export class HelpProvider { const outputPromise = new Promise((resolve) => { this.cp.stdout?.on('data', (data) => { try{ - // eslint-disable-next-line + // eslint-disable-next-line str += data.toString(); } catch(e){ resolve(''); @@ -92,7 +95,7 @@ export class HelpProvider { content?: string, redirect?: string } - + // forward request to R instance // below is just a complicated way of getting a http response from the help server let url = `http://localhost:${this.port}/${requestPath}`; @@ -159,7 +162,7 @@ export interface AliasProviderArgs { cwd?: string; // getAliases.R rScriptFile: string; - + persistentState: Memento; } @@ -283,7 +286,7 @@ export class AliasProvider { } // get from R - this.allPackageAliases = null; + 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-')); diff --git a/src/helpViewer/index.ts b/src/helpViewer/index.ts index 59a64fade..321bdce8b 100644 --- a/src/helpViewer/index.ts +++ b/src/helpViewer/index.ts @@ -41,7 +41,7 @@ export async function initializeHelp(context: vscode.ExtensionContext, rExtensio const rHelpOptions: HelpOptions = { webviewScriptPath: context.asAbsolutePath('/html/help/script.js'), webviewStylePath: context.asAbsolutePath('html/help/theme.css'), - rScriptFile: context.asAbsolutePath('R/getAliases.R'), + rScriptFile: context.asAbsolutePath('R/help/getAliases.R'), rPath: rPath, cwd: cwd, persistentState: persistentState diff --git a/src/rTerminal.ts b/src/rTerminal.ts index f1bba02b6..2fe936769 100644 --- a/src/rTerminal.ts +++ b/src/rTerminal.ts @@ -98,8 +98,8 @@ export async function createRTerm(preserveshow?: boolean): Promise { shellPath: termPath, shellArgs: termOpt, }; - const newRprofile = extensionContext.asAbsolutePath(path.join('R', '.Rprofile')); - const initR = extensionContext.asAbsolutePath(path.join('R', 'init.R')); + const newRprofile = extensionContext.asAbsolutePath(path.join('R', 'session', '.Rprofile')); + const initR = extensionContext.asAbsolutePath(path.join('R', 'session','init.R')); if (config().get('sessionWatcher')) { termOptions.env = { R_PROFILE_USER_OLD: process.env.R_PROFILE_USER, diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index 25a5afbd0..149d39290 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -6,7 +6,7 @@ import yaml = require('js-yaml'); import { RMarkdownManager, KnitWorkingDirectory, DisposableProcess } from './manager'; import { runTextInTerm } from '../rTerminal'; -import { rmdPreviewManager } from '../extension'; +import { extensionContext, rmdPreviewManager } from '../extension'; export let knitDir: KnitWorkingDirectory = util.config().get('rmarkdown.knit.defaults.knitWorkingDirectory') ?? undefined; @@ -29,18 +29,17 @@ export class RMarkdownKnitManager extends RMarkdownManager { private async renderDocument(rDocumentPath: string, docPath: string, docName: string, yamlParams: IYamlFrontmatter, outputFormat?: string): Promise { const openOutfile: boolean = util.config().get('rmarkdown.knit.openOutputFile') ?? false; const knitWorkingDir = this.getKnitDir(knitDir, docPath); - const knitWorkingDirText = knitWorkingDir ? `'${knitWorkingDir}'` : `NULL`; + const knitWorkingDirText = knitWorkingDir ? `${knitWorkingDir}` : `NULL`; 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 = - `knitr::opts_knit[['set']](root.dir = ${knitWorkingDirText});` + - `cat('${lim}', ` + - `${knitCommand}, ` + - `'${lim}',` + - `sep='')`; + const scriptValues = { + 'VSCR_KNIT_DIR': knitWorkingDirText, + 'VSCR_LIM': lim, + 'VSCR_KNIT_COMMAND': knitCommand + }; const callback = (dat: string) => { const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); @@ -67,7 +66,8 @@ export class RMarkdownKnitManager extends RMarkdownManager { { fileName: docName, filePath: rDocumentPath, - cmd: cmd, + scriptArgs: scriptValues, + scriptPath: extensionContext.asAbsolutePath('R/rmarkdown/knit.R'), rCmd: knitCommand, rOutputFormat: outputFormat, callback: callback diff --git a/src/rmarkdown/manager.ts b/src/rmarkdown/manager.ts index 0ef0a45a1..6bd98113c 100644 --- a/src/rmarkdown/manager.ts +++ b/src/rmarkdown/manager.ts @@ -20,7 +20,8 @@ const rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel( interface IKnitArgs { filePath: string; fileName: string; - cmd: string; + scriptArgs: Record; + scriptPath: string; rCmd?: string; rOutputFormat?: string; callback: (...args: unknown[]) => boolean; @@ -61,18 +62,23 @@ export abstract class RMarkdownManager { return await new Promise( (resolve, reject) => { - const cmd = args.cmd; + const scriptArgs = args.scriptArgs; + const scriptPath = args.scriptPath; const fileName = args.fileName; + const processArgs = [ `--silent`, `--slave`, `--no-save`, `--no-restore`, - `-e`, - cmd + `-f`, + `${scriptPath}` ]; const processOptions = { - env: process.env + env: { + ...process.env, + ...scriptArgs + } }; let childProcess: DisposableProcess; diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 4f72ecd8b..ca4d13f77 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -283,21 +283,20 @@ export class RMarkdownPreviewManager extends RMarkdownManager { private async previewDocument(filePath: string, fileName?: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn): Promise { const knitWorkingDir = this.getKnitDir(knitDir, filePath); - const knitWorkingDirText = knitWorkingDir ? `'${knitWorkingDir}'` : `NULL`; + const knitWorkingDirText = knitWorkingDir ? `${knitWorkingDir}` : `NULL`; 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 = ( - `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='')` - ); + const scriptValues = { + 'VSCR_KNIT_DIR' : knitWorkingDirText, + 'VSCR_LIM': lim, + 'VSCR_FILE_PATH': filePath.replace(/\\/g, '/'), + 'VSCR_OUTPUT_FILE': outputFile.replace(/\\/g, '/'), + 'VSCR_TMP_DIR': tmpDir().replace(/\\/g, '/') + }; + const callback = (dat: string, childProcess: DisposableProcess) => { const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); @@ -329,7 +328,8 @@ export class RMarkdownPreviewManager extends RMarkdownManager { { fileName: fileName, filePath: filePath, - cmd: cmd, + scriptPath: extensionContext.asAbsolutePath('R/rmarkdown/preview.R'), + scriptArgs: scriptValues, rOutputFormat: 'html preview', callback: callback, onRejection: onRejected diff --git a/src/session.ts b/src/session.ts index 2bcc9a7c9..67f0f8515 100644 --- a/src/session.ts +++ b/src/session.ts @@ -43,7 +43,7 @@ export function deploySessionWatcher(extensionPath: string): void { console.info(`[deploySessionWatcher] extensionPath: ${extensionPath}`); resDir = path.join(extensionPath, 'dist', 'resources'); - const initPath = path.join(extensionPath, 'R', 'init.R'); + const initPath = path.join(extensionPath, 'R', 'session', 'init.R'); const linkPath = path.join(homeExtDir(), 'init.R'); fs.writeFileSync(linkPath, `local(source("${initPath.replace(/\\/g, '\\\\')}", chdir = TRUE, local = TRUE))\n`); From b5cdbafa4b2865349a24a3c2e24103d5aab66a48 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Fri, 10 Sep 2021 08:58:49 +0000 Subject: [PATCH 23/60] Fix RMD requireNamespace (#784) Fixes regression with #781 due to a typo in requireNamespace --- R/rmarkdown/knit.R | 2 +- R/rmarkdown/preview.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/rmarkdown/knit.R b/R/rmarkdown/knit.R index 895fa27b5..734bd91a7 100644 --- a/R/rmarkdown/knit.R +++ b/R/rmarkdown/knit.R @@ -1,5 +1,5 @@ # requires rmarkdown package to run (and knitr) -if (!requireNamespace(rmarkdown, quietly = TRUE)) { +if (!requireNamespace("rmarkdown", quietly = TRUE)) { stop("Knitting requires the {rmarkdown} package.") } diff --git a/R/rmarkdown/preview.R b/R/rmarkdown/preview.R index ecf5e6cc4..e77a0f5d5 100644 --- a/R/rmarkdown/preview.R +++ b/R/rmarkdown/preview.R @@ -1,5 +1,5 @@ # requires rmarkdown package to run (and knitr) -if (!requireNamespace(rmarkdown, quietly = TRUE)) { +if (!requireNamespace("rmarkdown", quietly = TRUE)) { stop("Previewing documents requires the {rmarkdown} package.") } From 63323283599c447e1ffd1b93478ac14ce7b3a0c9 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Sat, 11 Sep 2021 05:12:53 +0000 Subject: [PATCH 24/60] Respect preview output format (#785) * Respect output format * Fix anchor tags --- R/rmarkdown/preview.R | 20 ++++++++++++++++++-- src/rmarkdown/preview.ts | 10 ++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/R/rmarkdown/preview.R b/R/rmarkdown/preview.R index e77a0f5d5..87bdea362 100644 --- a/R/rmarkdown/preview.R +++ b/R/rmarkdown/preview.R @@ -4,13 +4,29 @@ if (!requireNamespace("rmarkdown", quietly = TRUE)) { } # get values from extension-set env values - knit_dir <- Sys.getenv("VSCR_KNIT_DIR") lim <- Sys.getenv("VSCR_LIM") file_path <- Sys.getenv("VSCR_FILE_PATH") output_file_loc <- Sys.getenv("VSCR_OUTPUT_FILE") tmp_dir <- Sys.getenv("VSCR_TMP_DIR") +# if an output format ends up as html, we should not overwrite +# the format with rmarkdown::html_document() +set_html <- tryCatch( + expr = { + lines <- suppressWarnings(readLines(file_path, encoding = "UTF-8")) + out <- rmarkdown:::output_format_from_yaml_front_matter(lines) + output_format <- rmarkdown:::create_output_format(out$name, out$options) + if (!output_format$pandoc$to == "html") { + rmarkdown::html_document() + } else { + NULL + } + }, error = function(e) { + rmarkdown::html_document() + } +) + # set the knitr chunk eval directory # mainly affects source calls knitr::opts_knit[["set"]](root.dir = knit_dir) @@ -20,7 +36,7 @@ cat( lim, rmarkdown::render( file_path, - output_format = rmarkdown::html_document(), + output_format = set_html, output_file = output_file_loc, intermediates_dir = tmp_dir ), diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index ca4d13f77..e5a9466f6 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -71,7 +71,7 @@ class RMarkdownPreview extends vscode.Disposable { } private getHtmlContent(htmlContent: string): void { - let content = htmlContent.replace(/<(\w+)\s+(href|src)="(?!\w+:)/g, + let content = htmlContent.replace(/<(\w+)\s+(href|src)="(?!(\w+:)|#)/g, `<$1 $2="${String(this.panel.webview.asWebviewUri(vscode.Uri.file(tmpDir())))}/`); const re = new RegExp('.*', 'ms'); @@ -82,12 +82,11 @@ class RMarkdownPreview extends vscode.Disposable { content = `
${html}
`; } - this.htmlLightContent = content; - const $ = cheerio.load(content); - const chunkCol = String(config().get('rmarkdown.chunkBackgroundColor')); + this.htmlLightContent = $.html(); // make the output chunks a little lighter to stand out + const chunkCol = String(config().get('rmarkdown.chunkBackgroundColor')); const colReg = /[0-9.]+/g; const regOut = chunkCol.match(colReg); const outCol = `rgba(${regOut[0] ?? 100}, ${regOut[1] ?? 100}, ${regOut[2] ?? 100}, ${Number(regOut[3]) + 0.05 ?? .5})`; @@ -113,6 +112,9 @@ class RMarkdownPreview extends vscode.Disposable { pre > code { background: transparent; } + h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { + color: inherit; + } `; $('head').append(style); From 0777aa30042c746615873fbb13a51b2ea5323176 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 13 Sep 2021 05:44:32 +0000 Subject: [PATCH 25/60] Bump @types/vscode from 1.57.0 to 1.60.0 (#786) --- src/util.ts | 4 ++++ yarn.lock | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/util.ts b/src/util.ts index 9b14f13e5..a3984a4f8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -331,6 +331,10 @@ export class DummyMemento implements vscode.Memento { public async update(key: string, value: any): Promise { this.items.set(key, value); } + + public keys(): readonly string[] { + return Object.keys(this.items); + } } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any diff --git a/yarn.lock b/yarn.lock index 85c3c1b0b..d0773c266 100644 --- a/yarn.lock +++ b/yarn.lock @@ -261,9 +261,9 @@ integrity sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ== "@types/vscode@^1.57.0": - version "1.57.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.57.0.tgz#cc648e0573b92f725cd1baf2621f8da9f8bc689f" - integrity sha512-FeznBFtIDCWRluojTsi9c3LLcCHOXP5etQfBK42+ixo1CoEAchkw39tuui9zomjZuKfUVL33KZUDIwHZ/xvOkQ== + version "1.60.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.60.0.tgz#9330c317691b4f53441a18b598768faeeb71618a" + integrity sha512-wZt3VTmzYrgZ0l/3QmEbCq4KAJ71K3/hmMQ/nfpv84oH8e81KKwPEoQ5v8dNCxfHFVJ1JabHKmCvqdYOoVm1Ow== "@types/winreg@^1.2.30": version "1.2.30" From baeb5fd655ca2b2a63215e9540e4b4cbf386531d Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Tue, 14 Sep 2021 15:21:21 +0800 Subject: [PATCH 26/60] Extend providers to rmd (#787) --- src/completions.ts | 35 +++++++++++++++++++++++++++++++++++ src/extension.ts | 8 ++++---- src/rmarkdown/index.ts | 2 +- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/completions.ts b/src/completions.ts index 34e92f2b4..f45d75607 100644 --- a/src/completions.ts +++ b/src/completions.ts @@ -12,6 +12,7 @@ import { extendSelection } from './selection'; import { cleanLine } from './lineCache'; import { globalRHelp } from './extension'; import { config } from './util'; +import { getChunks } from './rmarkdown'; // Get with names(roxygen2:::default_tags()) @@ -33,6 +34,15 @@ export class HoverProvider implements vscode.HoverProvider { if(!session.globalenv){ return null; } + + if (document.languageId === 'rmd') { + const chunks = getChunks(document); + const chunk = chunks.find((chunk) => chunk.language === 'r' && chunk.startLine < position.line && chunk.endLine > position.line); + if (!chunk) { + return null; + } + } + const wordRange = document.getWordRangeAtPosition(position); const text = document.getText(wordRange); // use juggling check here for both @@ -50,6 +60,15 @@ export class HelpLinkHoverProvider implements vscode.HoverProvider { if(!config().get('helpPanel.enableHoverLinks')){ return null; } + + if (document.languageId === 'rmd') { + const chunks = getChunks(document); + const chunk = chunks.find((chunk) => chunk.language === 'r' && chunk.startLine < position.line && chunk.endLine > position.line); + if (!chunk) { + return null; + } + } + const re = /([a-zA-Z0-9._:])+/; const wordRange = document.getWordRangeAtPosition(position, re); const token = document.getText(wordRange); @@ -71,6 +90,14 @@ export class HelpLinkHoverProvider implements vscode.HoverProvider { export class StaticCompletionItemProvider implements vscode.CompletionItemProvider { provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem[] | undefined { + if (document.languageId === 'rmd') { + const chunks = getChunks(document); + const chunk = chunks.find((chunk) => chunk.language === 'r' && chunk.startLine < position.line && chunk.endLine > position.line); + if (!chunk) { + return undefined; + } + } + if (document.lineAt(position).text .substr(0, 2) === '#\'') { return roxygenTagCompletionItems; @@ -93,6 +120,14 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider return items; } + if (document.languageId === 'rmd') { + const chunks = getChunks(document); + const chunk = chunks.find((chunk) => chunk.language === 'r' && chunk.startLine < position.line && chunk.endLine > position.line); + if (!chunk) { + return items; + } + } + const trigger = completionContext.triggerCharacter; if (trigger === undefined) { diff --git a/src/extension.ts b/src/extension.ts index 4656413ee..fc1d64efc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -175,9 +175,9 @@ export async function activate(context: vscode.ExtensionContext): Promise Date: Thu, 23 Sep 2021 07:30:46 +0800 Subject: [PATCH 27/60] Bump nth-check from 2.0.0 to 2.0.1 (#795) Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/fb55/nth-check/releases) - [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1) --- updated-dependencies: - dependency-name: nth-check dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d0773c266..257614da2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2019,9 +2019,9 @@ npm-run-path@^4.0.1: path-key "^3.0.0" nth-check@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" - integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== + version "2.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== dependencies: boolbase "^1.0.0" From f2b8d0429f347e53232382953210ae8d147ccb89 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 23 Sep 2021 16:04:27 +0800 Subject: [PATCH 28/60] Update vscode and ag-grid version --- package.json | 6 +++--- yarn.lock | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a365b40dc..bef517b16 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "R Markdown" ], "engines": { - "vscode": "^1.57.0" + "vscode": "^1.60.0" }, "activationEvents": [ "onLanguage:r", @@ -1669,7 +1669,7 @@ "@types/node": "^14.17.3", "@types/node-fetch": "^2.5.10", "@types/showdown": "^1.9.3", - "@types/vscode": "^1.57.0", + "@types/vscode": "^1.60.0", "@types/winreg": "^1.2.30", "@types/ws": "^7.4.4", "@typescript-eslint/eslint-plugin": "4.25.0", @@ -1687,7 +1687,7 @@ "yamljs": "^0.3.0" }, "dependencies": { - "ag-grid-community": "^25.3.0", + "ag-grid-community": "^26.0.0", "bootstrap": "^5.0.1", "cheerio": "1.0.0-rc.10", "crypto": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index 257614da2..cdfd34891 100644 --- a/yarn.lock +++ b/yarn.lock @@ -260,7 +260,7 @@ resolved "https://registry.yarnpkg.com/@types/text-table/-/text-table-0.2.1.tgz#39c4d4a058a82f677392dfd09976e83d9b4c9264" integrity sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ== -"@types/vscode@^1.57.0": +"@types/vscode@^1.60.0": version "1.60.0" resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.60.0.tgz#9330c317691b4f53441a18b598768faeeb71618a" integrity sha512-wZt3VTmzYrgZ0l/3QmEbCq4KAJ71K3/hmMQ/nfpv84oH8e81KKwPEoQ5v8dNCxfHFVJ1JabHKmCvqdYOoVm1Ow== @@ -515,10 +515,10 @@ acorn@^8.2.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.0.tgz#af53266e698d7cffa416714b503066a82221be60" integrity sha512-ULr0LDaEqQrMFGyQ3bhJkLsbtrQ8QibAseGZeaSUiT/6zb9IvIkomWHJIvgvwad+hinRAgsI51JcWk2yvwyL+w== -ag-grid-community@^25.3.0: - version "25.3.0" - resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-25.3.0.tgz#9a0acce6d35f0c23313aa559dbd8acd737d5ef19" - integrity sha512-Xe4NZG0PP9kvhila1uHU4BeVfLDCWBmcuBYLBZ+49jvK+jYpuwdAjV3AwIlxpZGRR3WTdBUvUkSz9rmi2DRE3Q== +ag-grid-community@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-26.0.0.tgz#2f26d4e89f5d2b8264065e8a542c42973e1cb306" + integrity sha512-ewm640koOut+1nl+rDf/pUyM6/V2xbZ8T7VTzM0NZQNhn8TFG3grKs5iEzsKel8mwMWCSIO82xr356QNE11Hog== agent-base@6: version "6.0.2" From f6f713ac1e4cd5ab9469d55e81ee56bebf90f80e Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 23 Sep 2021 17:13:33 +0800 Subject: [PATCH 29/60] Update dependencies --- package.json | 8 +++----- yarn.lock | 41 ++++++++++++----------------------------- 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index bef517b16..079193cb5 100644 --- a/package.json +++ b/package.json @@ -1663,14 +1663,13 @@ "@types/ejs": "^3.0.6", "@types/express": "^4.17.12", "@types/fs-extra": "^9.0.11", - "@types/highlight.js": "^10.1.0", "@types/js-yaml": "^4.0.2", "@types/mocha": "^8.2.2", "@types/node": "^14.17.3", "@types/node-fetch": "^2.5.10", "@types/showdown": "^1.9.3", "@types/vscode": "^1.60.0", - "@types/winreg": "^1.2.30", + "@types/winreg": "^1.2.31", "@types/ws": "^7.4.4", "@typescript-eslint/eslint-plugin": "4.25.0", "@typescript-eslint/parser": "4.25.0", @@ -1688,12 +1687,11 @@ }, "dependencies": { "ag-grid-community": "^26.0.0", - "bootstrap": "^5.0.1", "cheerio": "1.0.0-rc.10", "crypto": "^1.0.1", "ejs": "^3.1.6", "fs-extra": "^10.0.0", - "highlight.js": "^10.7.2", + "highlight.js": "^11.2.0", "jquery": "^3.6.0", "jquery.json-viewer": "^1.4.0", "js-yaml": "^4.1.0", @@ -1702,7 +1700,7 @@ "showdown": "^1.9.1", "tree-kill": "^1.2.2", "vscode-languageclient": "^7.0.0", - "vsls": "^1.0.3015", + "vsls": "^1.0.4753", "winreg": "^1.2.4", "ws": "^7.4.6" } diff --git a/yarn.lock b/yarn.lock index cdfd34891..e52edccb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -187,13 +187,6 @@ dependencies: "@types/node" "*" -"@types/highlight.js@^10.1.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-10.1.0.tgz#89bb0c202997d7a90a07bd2ec1f7d00c56bb90b4" - integrity sha512-77hF2dGBsOgnvZll1vymYiNUtqJ8cJfXPD6GG/2M0aLRc29PkvB7Au6sIDjIEFcSICBhCh2+Pyq6WSRS7LUm6A== - dependencies: - highlight.js "*" - "@types/js-yaml@^4.0.2": version "4.0.3" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.3.tgz#9f33cd6fbf0d5ec575dc8c8fc69c7fec1b4eb200" @@ -265,10 +258,10 @@ resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.60.0.tgz#9330c317691b4f53441a18b598768faeeb71618a" integrity sha512-wZt3VTmzYrgZ0l/3QmEbCq4KAJ71K3/hmMQ/nfpv84oH8e81KKwPEoQ5v8dNCxfHFVJ1JabHKmCvqdYOoVm1Ow== -"@types/winreg@^1.2.30": - version "1.2.30" - resolved "https://registry.yarnpkg.com/@types/winreg/-/winreg-1.2.30.tgz#91d6710e536d345b9c9b017c574cf6a8da64c518" - integrity sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg= +"@types/winreg@^1.2.31": + version "1.2.31" + resolved "https://registry.yarnpkg.com/@types/winreg/-/winreg-1.2.31.tgz#914a7f076fd8f0f39964eb2e5c624c4fbdca77f7" + integrity sha512-SDatEMEtQ1cJK3esIdH6colduWBP+42Xw9Guq1sf/N6rM3ZxgljBduvZOwBsxRps/k5+Wwf5HJun6pH8OnD2gg== "@types/ws@^7.4.4": version "7.4.4" @@ -664,11 +657,6 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -bootstrap@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.0.1.tgz#e7939d599119dc818a90478a2a299bdaff037e09" - integrity sha512-Fl79+wsLOZKoiU345KeEaWD0ik8WKRI5zm0YSPj2oF1Qr+BO7z0fco6GbUtqjoG1h4VI89PeKJnMsMMVQdKKTw== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1518,15 +1506,10 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -highlight.js@*: - version "11.0.1" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.0.1.tgz#a78bafccd9aa297978799fe5eed9beb7ee1ef887" - integrity sha512-EqYpWyTF2s8nMfttfBA2yLKPNoZCO33pLS4MnbXQ4hECf1TKujCt1Kq7QAdrio7roL4+CqsfjqwYj4tYgq0pJQ== - -highlight.js@^10.7.2: - version "10.7.3" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" - integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== +highlight.js@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.2.0.tgz#a7e3b8c1fdc4f0538b93b2dc2ddd53a40c6ab0f0" + integrity sha512-JOySjtOEcyG8s4MLR2MNbLUyaXqUunmSnL2kdV/KuGJOmHZuAR5xC54Ko7goAXBWNhf09Vy3B+U7vR62UZ/0iw== htmlparser2@^6.1.0: version "6.1.0" @@ -2714,10 +2697,10 @@ vscode-test@^1.5.2: rimraf "^3.0.2" unzipper "^0.10.11" -vsls@^1.0.3015: - version "1.0.3015" - resolved "https://registry.yarnpkg.com/vsls/-/vsls-1.0.3015.tgz#346b034990aa2ff875c769fb7bf2abab085bb1d6" - integrity sha512-c+hG4X/aNdR4PM2nxUeooTEyDav8TQ8exfLSWIWp+h+us1jjhxGk2K+PDCrJxJmeIra/Ku4sdqMRv7P9hF6MRA== +vsls@^1.0.4753: + version "1.0.4753" + resolved "https://registry.yarnpkg.com/vsls/-/vsls-1.0.4753.tgz#1b0957fc987fddd2b4d8c03925d086fb701d11fa" + integrity sha512-hmrsMbhjuLoU8GgtVfqhbV4ZkGvDpLV2AFmzx+cCOGNra2qk0Q36dYkfwENqy/vJVQ/2/lhxcn+69FYnKQRhgg== dependencies: "@microsoft/servicehub-framework" "^2.6.74" From edc2e1aeafb807360707bd4e2dbfcd47368362ee Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 23 Sep 2021 17:47:40 +0800 Subject: [PATCH 30/60] Fix hljs usage --- src/helpViewer/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpViewer/index.ts b/src/helpViewer/index.ts index 321bdce8b..d3e0d4a13 100644 --- a/src/helpViewer/index.ts +++ b/src/helpViewer/index.ts @@ -509,7 +509,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer { - const styledCode = hljs.highlight('r', $(section).text() || ''); + const styledCode = hljs.highlight($(section).text() || '', { language: 'r' }); $(section).html(styledCode.value); }); } From e2c48ed56c4e8ece2428ee3cf5d17d10d55b2390 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 23 Sep 2021 17:54:39 +0800 Subject: [PATCH 31/60] Update highlight.js version --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 079193cb5..adad4dadf 100644 --- a/package.json +++ b/package.json @@ -1691,7 +1691,7 @@ "crypto": "^1.0.1", "ejs": "^3.1.6", "fs-extra": "^10.0.0", - "highlight.js": "^11.2.0", + "highlight.js": "^10.7.3", "jquery": "^3.6.0", "jquery.json-viewer": "^1.4.0", "js-yaml": "^4.1.0", diff --git a/yarn.lock b/yarn.lock index e52edccb4..fc55ecaa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1506,10 +1506,10 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -highlight.js@^11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.2.0.tgz#a7e3b8c1fdc4f0538b93b2dc2ddd53a40c6ab0f0" - integrity sha512-JOySjtOEcyG8s4MLR2MNbLUyaXqUunmSnL2kdV/KuGJOmHZuAR5xC54Ko7goAXBWNhf09Vy3B+U7vR62UZ/0iw== +highlight.js@^10.7.3: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== htmlparser2@^6.1.0: version "6.1.0" From 48fe454a7a8f55c72bb64a7cf728429a4ecef54b Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 23 Sep 2021 22:01:47 +0800 Subject: [PATCH 32/60] release 2.3.0 --- CHANGELOG.md | 16 ++++++++++++++++ package.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 017ddca07..ad8c0d590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ You can check all of our changes from [Release Page](https://github.com/REditorSupport/vscode-R/releases) +## [2.3.0](https://github.com/REditorSupport/vscode-R/releases/tag/v2.3.0) + +Enhancements + +* R Markdown preview now supports background rendering with progress bar, customizable + working directory, and smart knit button. (#765) +* `{rstudioapi}` emulation is enabled by default. (#769) +* A new setting `r.session.objectLengthLimit` is added to limit the output of the names of global objects with many named elements which might cause significant delay after inputs. (#778) +* `NA` and `Inf` could now be correctly displayed in the data viewer. (#780) +* User-specified R Markdown output format is now respected. (#785) + +Fixes + +* The security policy of WebView is relaxed to support `{flextable}` widgets. (#771) +* The R Markdown background rendering process could be properly terminated now. (#773) + ## [2.2.0](https://github.com/REditorSupport/vscode-R/releases/tag/v2.2.0) New Features diff --git a/package.json b/package.json index adad4dadf..c223167b8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "r", "displayName": "R", "description": "R Extension for Visual Studio Code", - "version": "2.2.0", + "version": "2.3.0", "author": "REditorSupport", "license": "SEE LICENSE IN LICENSE", "publisher": "Ikuyadeu", From ce40e740bc700a3dc6725cde683d6d8aeaf5ecc3 Mon Sep 17 00:00:00 2001 From: Olivier Benz Date: Wed, 29 Sep 2021 10:14:56 +0200 Subject: [PATCH 33/60] Update vsc.R (#803) * Update vsc.R * Modify url to allow proxied requests * Minor update * Fix message for webview Co-authored-by: Kun Ren --- R/session/vsc.R | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/R/session/vsc.R b/R/session/vsc.R index cc1728664..f2c4c1ed0 100644 --- a/R/session/vsc.R +++ b/R/session/vsc.R @@ -494,11 +494,24 @@ request_browser <- function(url, title, ..., viewer) { show_browser <- function(url, title = url, ..., viewer = getOption("vsc.browser", "Active")) { + proxy_uri <- Sys.getenv("VSCODE_PROXY_URI") + if (nzchar(proxy_uri)) { + is_base_path <- grepl("\\:\\d+$", url) + url <- sub("^https?\\://(127\\.0\\.0\\.1|localhost)(\\:)?", + sub("{port}", "", proxy_uri, fixed = TRUE), url) + if (is_base_path) { + url <- paste0(url, "/") + } + } if (grepl("^https?\\://(127\\.0\\.0\\.1|localhost)(\\:\\d+)?", url)) { request_browser(url = url, title = title, ..., viewer = viewer) } else if (grepl("^https?\\://", url)) { message( - "VSCode WebView only supports showing local http content.\n", + if (nzchar(proxy_uri)) { + "VSCode is not running on localhost but on a remote server.\n" + } else { + "VSCode WebView only supports showing local http content.\n" + }, "Opening in external browser..." ) request_browser(url = url, title = title, ..., viewer = FALSE) @@ -535,11 +548,24 @@ show_webview <- function(url, title, ..., viewer) { stop("Invalid object") } } + proxy_uri <- Sys.getenv("VSCODE_PROXY_URI") + if (nzchar(proxy_uri)) { + is_base_path <- grepl("\\:\\d+$", url) + url <- sub("^https?\\://(127\\.0\\.0\\.1|localhost)(\\:)?", + sub("{port}", "", proxy_uri, fixed = TRUE), url) + if (is_base_path) { + url <- paste0(url, "/") + } + } if (grepl("^https?\\://(127\\.0\\.0\\.1|localhost)(\\:\\d+)?", url)) { request_browser(url = url, title = title, ..., viewer = viewer) } else if (grepl("^https?\\://", url)) { message( - "VSCode WebView only supports showing local http content.\n", + if (nzchar(proxy_uri)) { + "VSCode is not running on localhost but on a remote server.\n" + } else { + "VSCode WebView only supports showing local http content.\n" + }, "Opening in external browser..." ) request_browser(url = url, title = title, ..., viewer = FALSE) From d6d7a6b2d0bdbb1fa6fd43d23389636499129e8f Mon Sep 17 00:00:00 2001 From: Julien Barnier Date: Wed, 29 Sep 2021 17:53:10 +0200 Subject: [PATCH 34/60] Reenable 'unsafe-eval' in script-src CSP (#805) --- src/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index 67f0f8515..7852880ea 100644 --- a/src/session.ts +++ b/src/session.ts @@ -632,7 +632,7 @@ export async function getWebviewHtml(webview: Webview, file: string, title: stri upgrade-insecure-requests; default-src https: data: filesystem:; style-src https: data: filesystem: 'unsafe-inline'; - script-src https: data: filesystem: 'unsafe-inline'; + script-src https: data: filesystem: 'unsafe-inline' 'unsafe-eval'; `; return ` From e4e7b44b92352bd1a673d5527764341e4c68c2f8 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 30 Sep 2021 17:10:01 +0800 Subject: [PATCH 35/60] Use r.session.viewers.viewColumn.helpPanel (#804) --- src/helpViewer/panel.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/helpViewer/panel.ts b/src/helpViewer/panel.ts index a345f035d..0a6489a22 100644 --- a/src/helpViewer/panel.ts +++ b/src/helpViewer/panel.ts @@ -4,7 +4,7 @@ import * as vscode from 'vscode'; import * as cheerio from 'cheerio'; import { HelpFile, RHelp } from '.'; -import { setContext, UriIcon } from '../util'; +import { setContext, UriIcon, config } from '../util'; //// Declaration of interfaces used/implemented by the Help Panel class // specified when creating a new help panel @@ -117,10 +117,13 @@ export class HelpPanel { } // shows (internal) help file object in webview - public async showHelpFile(helpFile: HelpFile|Promise, updateHistory = true, currentScrollY = 0, viewer?: string|any, preserveFocus: boolean = false): Promise{ + public async showHelpFile(helpFile: HelpFile | Promise, updateHistory = true, currentScrollY = 0, viewer?: vscode.ViewColumn | string, preserveFocus: boolean = false): Promise{ + if (viewer === undefined) { + viewer = config().get('session.viewers.viewColumn.helpPanel'); + } // update this.viewColumn if a valid viewer argument was supplied - if(typeof viewer === 'string'){ + if (typeof viewer === 'string'){ this.viewColumn = vscode.ViewColumn[String(viewer)]; } From 99af5362ad8811f3183934cdd09b05a4f4bba2b2 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 7 Oct 2021 06:21:01 +0000 Subject: [PATCH 36/60] Use cwd in knit process (#807) * Respect cwd * Start knit process from knitWorkingDirectory * Refine getKnitDir Co-authored-by: Kun Ren --- src/rmarkdown/knit.ts | 1 + src/rmarkdown/manager.ts | 10 ++++++---- src/rmarkdown/preview.ts | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index 149d39290..f490d600d 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -64,6 +64,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { return await this.knitWithProgress( { + workingDirectory: knitWorkingDir, fileName: docName, filePath: rDocumentPath, scriptArgs: scriptValues, diff --git a/src/rmarkdown/manager.ts b/src/rmarkdown/manager.ts index 6bd98113c..490eb039c 100644 --- a/src/rmarkdown/manager.ts +++ b/src/rmarkdown/manager.ts @@ -18,6 +18,7 @@ export interface IKnitRejection { const rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); interface IKnitArgs { + workingDirectory: string; filePath: string; fileName: string; scriptArgs: Record; @@ -35,8 +36,7 @@ export abstract class RMarkdownManager { // so that we can't spam the knit/preview button protected busyUriStore: Set = new Set(); - protected getKnitDir(knitDir: string, docPath?: string): string { - const currentDocumentWorkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(docPath) ?? vscode.window.activeTextEditor?.document?.uri)?.uri?.fsPath ?? undefined; + protected getKnitDir(knitDir: string, docPath: string): string { switch (knitDir) { // the directory containing the R Markdown document case KnitWorkingDirectory.documentDirectory: { @@ -44,6 +44,7 @@ export abstract class RMarkdownManager { } // the root of the current workspace case KnitWorkingDirectory.workspaceRoot: { + const currentDocumentWorkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(docPath) ?? vscode.window.activeTextEditor?.document?.uri)?.uri?.fsPath ?? undefined; return currentDocumentWorkspace.replace(/\\/g, '/').replace(/['"]/g, '\\"'); } // the working directory of the attached terminal, NYI @@ -74,11 +75,12 @@ export abstract class RMarkdownManager { `-f`, `${scriptPath}` ]; - const processOptions = { + const processOptions: cp.SpawnOptions = { env: { ...process.env, ...scriptArgs - } + }, + cwd: args.workingDirectory, }; let childProcess: DisposableProcess; diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index e5a9466f6..645776153 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -328,6 +328,7 @@ export class RMarkdownPreviewManager extends RMarkdownManager { return await this.knitWithProgress( { + workingDirectory: knitWorkingDir, fileName: fileName, filePath: filePath, scriptPath: extensionContext.asAbsolutePath('R/rmarkdown/preview.R'), From 31b7673213a4b815889cca9b6651f2206dc631ca Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 7 Oct 2021 14:31:37 +0800 Subject: [PATCH 37/60] Bump version --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad8c0d590..875e95bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ You can check all of our changes from [Release Page](https://github.com/REditorSupport/vscode-R/releases) +## [2.3.1](https://github.com/REditorSupport/vscode-R/releases/tag/v2.3.1) + +Enhancements: + +* Proxied requests are now supported to work with [code-server](https://github.com/cdr/code-server). (#275, #803) + +Fixes: + +* `unsafe-eval` is re-enabled in WebView Content Security Policy to make htmlwidgets such as plotly work. (#805) +* The help viewer now respects `r.session.viewers.viewColumn.helpPanel`. (#804) +* The working directory of the knit background process is now consistent with the knit working directory so that `.Rprofile` and `renv` setup are respected. (#807) + ## [2.3.0](https://github.com/REditorSupport/vscode-R/releases/tag/v2.3.0) Enhancements diff --git a/package.json b/package.json index c223167b8..3cd7b3498 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "r", "displayName": "R", "description": "R Extension for Visual Studio Code", - "version": "2.3.0", + "version": "2.3.1", "author": "REditorSupport", "license": "SEE LICENSE IN LICENSE", "publisher": "Ikuyadeu", From 8c8c7d5619386d2bb36119c65202282ed58fc86f Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 7 Oct 2021 14:37:14 +0800 Subject: [PATCH 38/60] release 2.3.1 --- src/rmarkdown/index.ts | 2 +- src/rmarkdown/knit.ts | 6 +++--- src/rmarkdown/preview.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rmarkdown/index.ts b/src/rmarkdown/index.ts index 1292ba7ad..438ad45bd 100644 --- a/src/rmarkdown/index.ts +++ b/src/rmarkdown/index.ts @@ -399,7 +399,7 @@ async function goToChunk(chunk: RMarkdownChunk) { // Move cursor 1 line below 'chunk start line' const line = chunk.startLine + 1; vscode.window.activeTextEditor.selection = new vscode.Selection(line, 0, line, 0); - await vscode.commands.executeCommand('revealLine', { lineNumber: line, at: 'center'}); + await vscode.commands.executeCommand('revealLine', { lineNumber: line, at: 'center' }); } export function goToPreviousChunk(chunks: RMarkdownChunk[] = _getChunks(), diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index f490d600d..b256ded80 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -109,7 +109,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { if (yamlParams?.['knit']) { const knitParam = yamlParams['knit']; knitCommand = outputFormat ? - `${knitParam}(${docPath}, output_format = '${outputFormat}')`: + `${knitParam}(${docPath}, output_format = '${outputFormat}')` : `${knitParam}(${docPath})`; } else if (!this.isREADME(docPath) && yamlParams?.['site']) { knitCommand = outputFormat ? @@ -127,7 +127,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { // check if the workspace of the document is a R Markdown site. // the definition of what constitutes an R Markdown site differs // depending on the type of R Markdown site (i.e., "simple" vs. blogdown sites) - private async findSiteParam(): Promise { + private async findSiteParam(): Promise { const rootFolder = vscode.workspace.workspaceFolders[0].uri.fsPath; const wad = vscode.window.activeTextEditor.document.uri.fsPath; const indexFile = (await vscode.workspace.findFiles(new vscode.RelativePattern(rootFolder, 'index.{Rmd,rmd, md}'), null, 1))?.[0]; @@ -136,7 +136,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { // 'Simple' R Markdown websites require all docs to be in the root folder if (fs.existsSync(siteRoot)) { return 'rmarkdown::render_site'; - // Other generators may allow for docs in subdirs + // Other generators may allow for docs in subdirs } else if (indexFile) { const indexData = this.getYamlFrontmatter(indexFile.fsPath); if (indexData?.['site']) { diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 645776153..6327534f4 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -292,7 +292,7 @@ export class RMarkdownPreviewManager extends RMarkdownManager { const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'ms'); const outputFile = path.join(tmpDir(), crypto.createHash('sha256').update(filePath).digest('hex') + '.html'); const scriptValues = { - 'VSCR_KNIT_DIR' : knitWorkingDirText, + 'VSCR_KNIT_DIR': knitWorkingDirText, 'VSCR_LIM': lim, 'VSCR_FILE_PATH': filePath.replace(/\\/g, '/'), 'VSCR_OUTPUT_FILE': outputFile.replace(/\\/g, '/'), From 9c149a98bcd7c5fe57e8edd043ed6c4f3e269769 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Mon, 11 Oct 2021 19:06:32 +0800 Subject: [PATCH 39/60] Httpgd plot viewer respects r.session.viewers.viewColumn.plot (#816) --- src/plotViewer/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/plotViewer/index.ts b/src/plotViewer/index.ts index 2e1199245..d9d5e7146 100644 --- a/src/plotViewer/index.ts +++ b/src/plotViewer/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -60,8 +61,7 @@ export class HttpgdManager { this.viewerOptions = { parent: this, htmlRoot: htmlRoot, - preserveFocus: true, - viewColumn: vscode.ViewColumn.Two + preserveFocus: true }; } @@ -345,7 +345,8 @@ export class HttpgdViewer implements IHttpgdViewer { this.checkStateDelayed(); } }); - this.customOverwriteCssPath = config().get('plot.customStyleOverwrites', ''); + const conf = config(); + this.customOverwriteCssPath = conf.get('plot.customStyleOverwrites', ''); const localResourceRoots = ( this.customOverwriteCssPath ? [extensionContext.extensionUri, vscode.Uri.file(path.dirname(this.customOverwriteCssPath))] : @@ -355,7 +356,7 @@ export class HttpgdViewer implements IHttpgdViewer { this.htmlTemplate = fs.readFileSync(path.join(this.htmlRoot, 'index.ejs'), 'utf-8'); this.smallPlotTemplate = fs.readFileSync(path.join(this.htmlRoot, 'smallPlot.ejs'), 'utf-8'); this.showOptions = { - viewColumn: options.viewColumn ?? vscode.ViewColumn.Two, + viewColumn: options.viewColumn ?? vscode.ViewColumn[conf.get('session.viewers.viewColumn.plot') || 'Two'], preserveFocus: !!options.preserveFocus }; this.webviewOptions = { From 98f8dcaa08f2827a5e13c345764311267d952647 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Mon, 11 Oct 2021 19:06:47 +0800 Subject: [PATCH 40/60] Completely replace View (#818) --- R/session/init.R | 8 +------- R/session/vsc.R | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/R/session/init.R b/R/session/init.R index cd6b67065..b44cc6c96 100644 --- a/R/session/init.R +++ b/R/session/init.R @@ -64,13 +64,7 @@ init_last <- function() { .vsc.browser <- .vsc$show_browser .vsc.viewer <- .vsc$show_viewer .vsc.page_viewer <- .vsc$show_page_viewer - - # assign functions that are optional: - for (funcName in c("View")) { - if (funcName %in% ls(.vsc)) { - assign(funcName, .vsc[[funcName]]) - } - } + View <- .vsc.view environment() }) attach(exports, name = .vsc.name, warn.conflicts = FALSE) diff --git a/R/session/vsc.R b/R/session/vsc.R index f2c4c1ed0..6395772e1 100644 --- a/R/session/vsc.R +++ b/R/session/vsc.R @@ -454,7 +454,7 @@ if (show_view) { } } - View <- show_dataview + rebind("View", show_dataview, "utils") } attach <- function() { From b7c3058ea54e9ab034c65d2a3e4fea12011ab959 Mon Sep 17 00:00:00 2001 From: Manuel Hentschel <53863351+ManuelHentschel@users.noreply.github.com> Date: Mon, 11 Oct 2021 16:27:56 +0200 Subject: [PATCH 41/60] Change help cache default (#819) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cd7b3498..5d812efa6 100644 --- a/package.json +++ b/package.json @@ -1349,7 +1349,7 @@ "Store on a per workspace basis", "Store globally" ], - "default": "Global" + "default": "None" }, "r.helpPanel.rpath": { "type": "string", From 1b7a56909300c998f8efa06e91de0ebfa52b98d3 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Mon, 11 Oct 2021 22:28:20 +0800 Subject: [PATCH 42/60] browser handles `file://` (#817) * browser handles file:// * No need to check before sub --- R/session/vsc.R | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/R/session/vsc.R b/R/session/vsc.R index 6395772e1..ed9483813 100644 --- a/R/session/vsc.R +++ b/R/session/vsc.R @@ -515,21 +515,24 @@ show_browser <- function(url, title = url, ..., "Opening in external browser..." ) request_browser(url = url, title = title, ..., viewer = FALSE) - } else if (file.exists(url)) { - url <- normalizePath(url, "/", mustWork = TRUE) - if (grepl("\\.html?$", url, ignore.case = TRUE)) { - message( - "VSCode WebView has restricted access to local file.\n", - "Opening in external browser..." - ) - request_browser(url = path_to_uri(url), - title = title, ..., viewer = FALSE) + } else { + path <- sub("^file\\://", "", url) + if (file.exists(path)) { + path <- normalizePath(path, "/", mustWork = TRUE) + if (grepl("\\.html?$", path, ignore.case = TRUE)) { + message( + "VSCode WebView has restricted access to local file.\n", + "Opening in external browser..." + ) + request_browser(url = path_to_uri(path), + title = title, ..., viewer = FALSE) + } else { + request("dataview", source = "object", type = "txt", + title = title, file = path, viewer = viewer) + } } else { - request("dataview", source = "object", type = "txt", - title = title, file = url, viewer = viewer) + stop("File not exists") } - } else { - stop("File not exists") } } From 29801b790c964a2588b0f244933f2d854361f01f Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Tue, 12 Oct 2021 23:32:29 +0800 Subject: [PATCH 43/60] Add `r.session.levelOfObjectDetail=Normal` for `max.level=1` (#815) * Add r.session.levelOfObjectDetail=Normal for max.level=1 * Add object timeout * Update capture_str --- R/session/vsc.R | 79 +++++++++++++++++++++++++++++-------------------- package.json | 9 +++++- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/R/session/vsc.R b/R/session/vsc.R index ed9483813..b09921e30 100644 --- a/R/session/vsc.R +++ b/R/session/vsc.R @@ -15,19 +15,24 @@ load_settings <- function() { return(FALSE) } + setting <- function(x, ...) { + switch(EXPR = x, ..., x) + } + mapping <- quote(list( vsc.use_httpgd = plot$useHttpgd, vsc.show_object_size = workspaceViewer$showObjectSize, vsc.rstudioapi = session$emulateRStudioAPI, - vsc.str.max.level = session$levelOfObjectDetail, + vsc.str.max.level = setting(session$levelOfObjectDetail, Minimal = 0, Normal = 1, Detailed = 2), vsc.object_length_limit = session$objectLengthLimit, + vsc.object_timeout = session$objectTimeout, vsc.globalenv = session$watchGlobalEnvironment, - vsc.plot = session$viewers$viewColumn$plot, - vsc.browser = session$viewers$viewColumn$browser, - vsc.viewer = session$viewers$viewColumn$viewer, - vsc.page_viewer = session$viewers$viewColumn$pageViewer, - vsc.view = session$viewers$viewColumn$view, - vsc.helpPanel = session$viewers$viewColumn$helpPanel + vsc.plot = setting(session$viewers$viewColumn$plot, Disable = FALSE), + vsc.browser = setting(session$viewers$viewColumn$browser, Disable = FALSE), + vsc.viewer = setting(session$viewers$viewColumn$viewer, Disable = FALSE), + vsc.page_viewer = setting(session$viewers$viewColumn$pageViewer, Disable = FALSE), + vsc.view = setting(session$viewers$viewColumn$view, Disable = FALSE), + vsc.helpPanel = setting(session$viewers$viewColumn$helpPanel, Disable = FALSE) )) vsc_settings <- tryCatch(jsonlite::read_json(settings_file), error = function(e) { @@ -41,21 +46,7 @@ load_settings <- function() { ops <- eval(mapping, vsc_settings) # exclude options set by user on startup - ops <- ops[!(names(ops) %in% user_options)] - - # translate VS Code setting values to R option values - r_options <- lapply(ops, function(x) { - if (is.character(x) && length(x) == 1) { - switch(EXPR = x, - "Disable" = FALSE, - "Minimal" = 0, - "Detailed" = 2, - x - ) - } else { - x - } - }) + r_options <- ops[!(names(ops) %in% user_options)] options(r_options) } @@ -88,15 +79,27 @@ request <- function(command, ...) { cat(get_timestamp(), file = request_lock_file) } +try_catch_timeout <- function(expr, timeout = Inf, ...) { + expr <- substitute(expr) + envir <- parent.frame() + setTimeLimit(timeout, transient = TRUE) + on.exit(setTimeLimit()) + tryCatch(eval(expr, envir), ...) +} + capture_str <- function(object, max.level = getOption("vsc.str.max.level", 0)) { + paste0(utils::capture.output( + utils::str(object, + max.level = max.level, + give.attr = FALSE, + vec.len = 1 + ) + ), collapse = "\n") +} + +try_capture_str <- function(object, max.level = getOption("vsc.str.max.level", 0)) { tryCatch( - paste0(utils::capture.output( - utils::str(object, - max.level = max.level, - give.attr = FALSE, - vec.len = 1 - ) - ), collapse = "\n"), + capture_str(object, max.level = max.level), error = function(e) { paste0(class(object), collapse = ", ") } @@ -135,6 +138,7 @@ inspect_env <- function(env, cache) { is_active <- rlang::env_binding_are_active(env, all_names) show_object_size <- getOption("vsc.show_object_size", FALSE) object_length_limit <- getOption("vsc.object_length_limit", 2000) + object_timeout <- getOption("vsc.object_timeout", 50) / 1000 str_max_level <- getOption("vsc.str.max.level", 0) objs <- lapply(all_names, function(name) { if (is_promise[[name]]) { @@ -174,9 +178,20 @@ inspect_env <- function(env, cache) { } if (length(obj) > object_length_limit) { - info$str <- scalar(trimws(capture_str(obj, 0))) + info$str <- scalar(trimws(try_capture_str(obj, 0))) } else { - info$str <- scalar(trimws(capture_str(obj, str_max_level))) + info_str <- NULL + if (str_max_level > 0) { + info_str <- try_catch_timeout( + capture_str(obj, str_max_level), + timeout = object_timeout, + error = function(e) NULL + ) + } + if (is.null(info_str)) { + info_str <- try_capture_str(obj, 0) + } + info$str <- scalar(trimws(info_str)) obj_names <- if (is.object(obj)) { .DollarNames(obj, pattern = "") } else if (is.recursive(obj)) { @@ -401,7 +416,7 @@ if (show_view) { type = typeof(obj), length = length(obj), size = as.integer(object.size(obj)), - value = trimws(capture_str(obj, 0)), + value = trimws(try_capture_str(obj, 0)), stringsAsFactors = FALSE, check.names = FALSE ) diff --git a/package.json b/package.json index 5d812efa6..8a01a2049 100644 --- a/package.json +++ b/package.json @@ -1403,17 +1403,24 @@ "default": 2000, "markdownDescription": "The upper limit of object length to show object details in workspace viewer and provide session symbol completion. Decrease this value if you experience significant delay after executing R commands caused by large global objects with many elements. Changes the option `vsc.object_length_limit` in R. Requires `#r.sessionWatcher#` to be set to `true`." }, + "r.session.objectTimeout": { + "type": "integer", + "default": 50, + "markdownDescription": "The maximum number of milliseconds to get information of a single object in the global environment. Decrease this value if you experience significant delay after executing R commands caused by large global objects with many elements. Changes the option `vsc.object_timeout` in R. Requires `#r.sessionWatcher#` to be set to `true`." + }, "r.session.levelOfObjectDetail": { "type": "string", "markdownDescription": "How much of the object to show on hover, autocompletion, and in the workspace viewer? Changes the option `vsc.str.max.level` in R. Requires `#r.sessionWatcher#` to be set to `true`.", "default": "Minimal", "enum": [ "Minimal", + "Normal", "Detailed" ], "enumDescriptions": [ "Display literal values and object types only.", - "Display list content, data frame column values, and example values." + "Display the top level of list content, data frame column values, and example values.", + "Display the top two levels of list content, data frame column values, and example values. This option may cause notable delay after each user input in the terminal." ] }, "r.session.emulateRStudioAPI": { From 155bb157b2573a00240824452646d9f54d4c1663 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Wed, 13 Oct 2021 14:10:59 +0800 Subject: [PATCH 44/60] Update address --- R/session/vsc.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/session/vsc.R b/R/session/vsc.R index b09921e30..50809d75c 100644 --- a/R/session/vsc.R +++ b/R/session/vsc.R @@ -125,8 +125,8 @@ rebind <- function(sym, value, ns) { } address <- function(x) { - info <- utils::capture.output(.Internal(inspect(x, 0L))) - gsub("@([a-z0-9]+)\\s+.+", "\\1", info[[1]]) + info <- utils::capture.output(.Internal(inspect(x, 0L, 0L))) + sub("@([a-z0-9]+)\\s+.+", "\\1", info[[1]]) } globalenv_cache <- new.env(parent = emptyenv()) From cbeda76dfc3419e116af5b2404c2a769ba4663e9 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 22 Oct 2021 13:58:23 +0800 Subject: [PATCH 45/60] Check workspace folder with both original and real path (#827) --- src/session.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/session.ts b/src/session.ts index 7852880ea..ed08e4dfe 100644 --- a/src/session.ts +++ b/src/session.ts @@ -660,13 +660,21 @@ export async function getWebviewHtml(webview: Webview, file: string, title: stri function isFromWorkspace(dir: string) { if (workspace.workspaceFolders === undefined) { - const rel = path.relative(os.homedir(), dir); + let rel = path.relative(os.homedir(), dir); + if (rel === '') { + return true; + } + rel = path.relative(fs.realpathSync(os.homedir()), dir); if (rel === '') { return true; } } else { for (const folder of workspace.workspaceFolders) { - const rel = path.relative(folder.uri.fsPath, dir); + let rel = path.relative(folder.uri.fsPath, dir); + if (!rel.startsWith('..') && !path.isAbsolute(rel)) { + return true; + } + rel = path.relative(fs.realpathSync(folder.uri.fsPath), dir); if (!rel.startsWith('..') && !path.isAbsolute(rel)) { return true; } From 9092f8f840303c7bd3ebe379413f615f9e562328 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Sat, 23 Oct 2021 00:55:53 +0800 Subject: [PATCH 46/60] release 2.3.2 --- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 875e95bd6..76df6ad08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ You can check all of our changes from [Release Page](https://github.com/REditorSupport/vscode-R/releases) +## [2.3.2](https://github.com/REditorSupport/vscode-R/releases/tag/v2.3.2) + +Enhancements: + +* `.vsc.browser()` now handles `file://` urls. (#817) +* `r.session.levelOfObjectDetail` gains a `Normal` value for the session watcher to write only first level structure of global objects for performance. (#815) +* Session watcher now supports workspace folder as symlinks. (#827) + +Fixes: + +* Httpgd plot viewer respects the view column specified by `r.session.viewers.viewColumn.plot` setting (#816) +* `View` is completed replaced so that `tibble::view()` could +trigger data viewer (#818) +* Help cache is disabled between sessions (#819) + ## [2.3.1](https://github.com/REditorSupport/vscode-R/releases/tag/v2.3.1) Enhancements: diff --git a/package.json b/package.json index 8a01a2049..327f103c2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "r", "displayName": "R", "description": "R Extension for Visual Studio Code", - "version": "2.3.1", + "version": "2.3.2", "author": "REditorSupport", "license": "SEE LICENSE IN LICENSE", "publisher": "Ikuyadeu", From 1e400e08d970cd357635fc52701a25e981e51c00 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Tue, 26 Oct 2021 16:47:14 +0800 Subject: [PATCH 47/60] Add R info to status bar item text and tooltip (#836) * Add R info to status bar item text and tooltip * Remove attach_time and add command * Update shareSession.ts --- R/session/vsc.R | 8 +++++++- src/extension.ts | 2 +- src/liveShare/shareSession.ts | 21 +++++++++++++++++---- src/session.ts | 7 ++++++- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/R/session/vsc.R b/R/session/vsc.R index 50809d75c..8943e61cb 100644 --- a/R/session/vsc.R +++ b/R/session/vsc.R @@ -478,8 +478,14 @@ attach <- function() { rstudioapi_util_env$update_addin_registry(addin_registry) } request("attach", + version = sprintf("%s.%s", R.version$major, R.version$minor), tempdir = tempdir, - plot = getOption("vsc.plot", "Two") + plot = getOption("vsc.plot", "Two"), + info = list( + command = commandArgs()[[1L]], + version = R.version.string, + start_time = format(file.info(tempdir)$ctime) + ) ) if (identical(names(dev.cur()), "httpgd")) { .vsc$request("httpgd", url = httpgd::hgd_url()) diff --git a/src/extension.ts b/src/extension.ts index fc1d64efc..414ffa2a1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -213,7 +213,7 @@ export async function activate(context: vscode.ExtensionContext): Promise Date: Fri, 29 Oct 2021 08:05:36 +0200 Subject: [PATCH 48/60] get knit command from settings (#841) * get knit command from settings * configuration option description * Refine description * Use a default value * Update comment Co-authored-by: Kun Ren --- package.json | 5 +++++ src/rmarkdown/knit.ts | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 327f103c2..861c79171 100644 --- a/package.json +++ b/package.json @@ -1316,6 +1316,11 @@ "default": false, "markdownDescription": "Should the output file be opened automatically when using knit?\n\nRequires `#r.rmarkdown.knit.useBackgroundProcess#` to be set to `true`." }, + "r.rmarkdown.knit.command": { + "type": "string", + "default": "rmarkdown::render", + "markdownDescription": "Command used to knit a Rmd file if not specified by the frontmatter." + }, "r.rmarkdown.knit.defaults.knitWorkingDirectory": { "type": "string", "default": "document directory", diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index b256ded80..55aeb1f2a 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -105,7 +105,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { } // precedence: - // knit > site > none + // knit > site > configuration if (yamlParams?.['knit']) { const knitParam = yamlParams['knit']; knitCommand = outputFormat ? @@ -116,9 +116,10 @@ export class RMarkdownKnitManager extends RMarkdownManager { `rmarkdown::render_site(${docPath}, output_format = '${outputFormat}')` : `rmarkdown::render_site(${docPath})`; } else { + const cmd = util.config().get('rmarkdown.knit.command'); knitCommand = outputFormat ? - `rmarkdown::render(${docPath}, output_format = '${outputFormat}')` : - `rmarkdown::render(${docPath})`; + `${cmd}(${docPath}, output_format = '${outputFormat}')` : + `${cmd}(${docPath})`; } return knitCommand.replace(/['"]/g, '\''); From 12f3b205af2eed5c86e3fad1f91989f7dca570be Mon Sep 17 00:00:00 2001 From: Manuel Hentschel <53863351+ManuelHentschel@users.noreply.github.com> Date: Fri, 29 Oct 2021 08:11:58 +0200 Subject: [PATCH 49/60] Fix package installation (#846) * Fix package installation * Fix typo Co-authored-by: Kun Ren --- src/helpViewer/packages.ts | 6 +++--- src/util.ts | 28 ++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/helpViewer/packages.ts b/src/helpViewer/packages.ts index a1d813f6f..2443c58be 100644 --- a/src/helpViewer/packages.ts +++ b/src/helpViewer/packages.ts @@ -215,7 +215,7 @@ export class PackageManager { const prompt = `Are you sure you want to remove package ${pkgName}?`; if(await getConfirmation(prompt, confirmation, cmd)){ - await executeAsTask('Remove Package', rPath, args); + await executeAsTask('Remove Package', rPath, args, true); return true; } else{ return false; @@ -234,7 +234,7 @@ export class PackageManager { const prompt = `Are you sure you want to install package${pluralS}: ${pkgNames.join(', ')}?`; if(skipConfirmation || await getConfirmation(prompt, confirmation, cmd)){ - await executeAsTask('Install Package', rPath, args); + await executeAsTask('Install Package', rPath, args, true); return true; } return false; @@ -249,7 +249,7 @@ export class PackageManager { const prompt = 'Are you sure you want to update all installed packages? This might take some time!'; if(skipConfirmation || await getConfirmation(prompt, confirmation, cmd)){ - await executeAsTask('Update Packages', rPath, args); + await executeAsTask('Update Packages', rPath, args, true); return true; } else{ return false; diff --git a/src/util.ts b/src/util.ts index a3984a4f8..d38d58bd6 100644 --- a/src/util.ts +++ b/src/util.ts @@ -198,15 +198,27 @@ export async function getConfirmation(prompt: string, confirmation?: string, det } // executes a given command as shell task -// is more transparent thatn background processes without littering the integrated terminals +// is more transparent than background processes without littering the integrated terminals // is not intended for actual user interaction -export async function executeAsTask(name: string, command: string, args?: string[]): Promise { - const taskDefinition = { type: 'shell' }; - const quotedArgs = args.map(arg => { return { value: arg, quoting: vscode.ShellQuoting.Weak }; }); - const taskExecution = new vscode.ShellExecution( - command, - quotedArgs - ); +export async function executeAsTask(name: string, process: string, args?: string[], asProcess?: true): Promise; +export async function executeAsTask(name: string, command: string, args?: string[], asProcess?: false): Promise; +export async function executeAsTask(name: string, cmdOrProcess: string, args?: string[], asProcess: boolean = false): Promise { + let taskDefinition: vscode.TaskDefinition; + let taskExecution: vscode.ShellExecution | vscode.ProcessExecution; + if(asProcess){ + taskDefinition = { type: 'process'}; + taskExecution = new vscode.ProcessExecution( + cmdOrProcess, + args + ); + } else{ + taskDefinition = { type: 'shell' }; + const quotedArgs = args.map(arg => { return { value: arg, quoting: vscode.ShellQuoting.Weak }; }); + taskExecution = new vscode.ShellExecution( + cmdOrProcess, + quotedArgs + ); + } const task = new vscode.Task( taskDefinition, vscode.TaskScope.Global, From 2af23c461ec561d10e6cd0fa9d7178e31f0c9f3a Mon Sep 17 00:00:00 2001 From: Manuel Hentschel <53863351+ManuelHentschel@users.noreply.github.com> Date: Fri, 29 Oct 2021 08:17:17 +0200 Subject: [PATCH 50/60] Add support for indented Roxygen (#847) * Add support for indented Roxygen * Exit roxygen after 2 empty lines * Fix regex --- src/extension.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 414ffa2a1..fe941b584 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -157,7 +157,13 @@ export async function activate(context: vscode.ExtensionContext): Promise Date: Sun, 31 Oct 2021 10:41:33 +0100 Subject: [PATCH 51/60] Syntax highlighting for indented roxygen (#850) --- syntax/r.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syntax/r.json b/syntax/r.json index 11b528274..69e57cb08 100644 --- a/syntax/r.json +++ b/syntax/r.json @@ -462,7 +462,7 @@ "roxygen": { "patterns": [ { - "begin": "^(#')\\s*", + "begin": "^\\s*(#')\\s*", "beginCaptures": { "1": { "name": "punctuation.definition.comment.r" From 9947208619de91d30d0c6b61b3ee07f387787f6b Mon Sep 17 00:00:00 2001 From: Manuel Hentschel <53863351+ManuelHentschel@users.noreply.github.com> Date: Mon, 1 Nov 2021 00:25:36 +0100 Subject: [PATCH 52/60] Use new terminal API (#851) --- package.json | 11 ++++++++- src/extension.ts | 11 +++++++++ src/rTerminal.ts | 61 ++++++++++++++++++++++++------------------------ 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 861c79171..1675c4b9d 100644 --- a/package.json +++ b/package.json @@ -61,10 +61,19 @@ "onCommand:r.helpPanel.back", "onCommand:r.helpPanel.forward", "onCommand:r.helpPanel.openForSelection", - "onWebviewPanel:rhelp" + "onWebviewPanel:rhelp", + "onTerminalProfile:r.terminal-profile" ], "main": "./out/extension", "contributes": { + "terminal": { + "profiles": [ + { + "title": "R Terminal", + "id": "r.terminal-profile" + } + ] + }, "viewsContainers": { "activitybar": [ { diff --git a/src/extension.ts b/src/extension.ts index fe941b584..29963b633 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -168,6 +168,17 @@ export async function activate(context: vscode.ExtensionContext): Promise { @@ -82,41 +82,40 @@ export async function runFromLineToEnd(): Promise { await runTextInTerm(text); } - -export async function createRTerm(preserveshow?: boolean): Promise { - const termName = 'R Interactive'; +export async function makeTerminalOptions(): Promise { const termPath = await getRterm(); - console.info(`termPath: ${termPath}`); - if (termPath === undefined) { - return undefined; + const shellArgs: string[] = config().get('rterm.option'); + const termOptions: vscode.TerminalOptions = { + name: 'R Interactive', + shellPath: termPath, + shellArgs: shellArgs, + }; + const newRprofile = extensionContext.asAbsolutePath(path.join('R', 'session', '.Rprofile')); + const initR = extensionContext.asAbsolutePath(path.join('R', 'session','init.R')); + if (config().get('sessionWatcher')) { + termOptions.env = { + R_PROFILE_USER_OLD: process.env.R_PROFILE_USER, + R_PROFILE_USER: newRprofile, + VSCODE_INIT_R: initR, + VSCODE_WATCHER_DIR: homeExtDir() + }; } - const termOpt: string[] = config().get('rterm.option'); - pathExists(termPath, (err, exists) => { - if (exists) { - const termOptions: vscode.TerminalOptions = { - name: termName, - shellPath: termPath, - shellArgs: termOpt, - }; - const newRprofile = extensionContext.asAbsolutePath(path.join('R', 'session', '.Rprofile')); - const initR = extensionContext.asAbsolutePath(path.join('R', 'session','init.R')); - if (config().get('sessionWatcher')) { - termOptions.env = { - R_PROFILE_USER_OLD: process.env.R_PROFILE_USER, - R_PROFILE_USER: newRprofile, - VSCODE_INIT_R: initR, - VSCODE_WATCHER_DIR: homeExtDir() - }; - } - rTerm = vscode.window.createTerminal(termOptions); - rTerm.show(preserveshow); + return termOptions; +} - return true; - } +export async function createRTerm(preserveshow?: boolean): Promise { + const termOptions = await makeTerminalOptions(); + const termPath = termOptions.shellPath; + if(!termPath){ + void vscode.window.showErrorMessage('Could not find R path. Please check r.term and r.path setting.'); + return false; + } else if(!fs.existsSync(termPath)){ void vscode.window.showErrorMessage(`Cannot find R client at ${termPath}. Please check r.rterm setting.`); - return false; - }); + } + rTerm = vscode.window.createTerminal(termOptions); + rTerm.show(preserveshow); + return true; } export async function restartRTerminal(): Promise{ From 1706790046ab5c7cf450db6ac5807bdf2eed2dc0 Mon Sep 17 00:00:00 2001 From: Jan Meis <55784605+jan-imbi@users.noreply.github.com> Date: Sun, 7 Nov 2021 17:10:35 +0100 Subject: [PATCH 53/60] Add backtick to list of quote characters for syntax highlighting. (#859) * Add backtick to list of quote characters for syntax highlighting. * backticks denote nonstandard variable names and should therefore be coloured like variables --- syntax/r.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/syntax/r.json b/syntax/r.json index 69e57cb08..821fadbe3 100644 --- a/syntax/r.json +++ b/syntax/r.json @@ -248,6 +248,27 @@ "name": "constant.character.escape.r" } ] + }, + { + "begin": "`", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.r" + } + }, + "end": "`", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.r" + } + }, + "name": "variable.parameter.r", + "patterns": [ + { + "match": "\\\\.", + "name": "variable.parameter.r" + } + ] } ] }, From 130f95ccaa917142315190043753dbd05f06d870 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Mon, 8 Nov 2021 20:49:47 +0800 Subject: [PATCH 54/60] Fix detecting yaml frontmatter (#856) * Fix detecting yaml frontmatter * Simplify condition --- src/rmarkdown/knit.ts | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index 55aeb1f2a..3bf0957a3 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -78,16 +78,37 @@ export class RMarkdownKnitManager extends RMarkdownManager { } private getYamlFrontmatter(docPath: string): IYamlFrontmatter { - const parseData = fs.readFileSync(docPath, 'utf8'); - const yamlDat = /(?<=(---)).*(?=(---))/gs.exec( - parseData - ); + const text = fs.readFileSync(docPath, 'utf8'); + const lines = text.split('\n'); + let startLine = -1; + let endLine = -1; + for (let i = 0; i < lines.length; i++) { + if (/\S/.test(lines[i])) { + if (startLine < 0) { + if (lines[i].startsWith('---')) { + startLine = i; + } else { + break; + } + } else { + if (lines[i].startsWith('---')) { + endLine = i; + break; + } + } + } + } + + let yamlText = undefined; + if (startLine + 1 < endLine) { + yamlText = lines.slice(startLine + 1, endLine).join('\n'); + } let paramObj = {}; - if (yamlDat) { + if (yamlText) { try { paramObj = yaml.load( - yamlDat[0] + yamlText ); } catch (e) { console.error(`Could not parse YAML frontmatter for "${docPath}". Error: ${String(e)}`); From c15a9efd38fe344bfdd27cb670e4c1c3eb528219 Mon Sep 17 00:00:00 2001 From: Manuel Hentschel <53863351+ManuelHentschel@users.noreply.github.com> Date: Thu, 11 Nov 2021 08:53:57 +0100 Subject: [PATCH 55/60] Auto refresh help (#863) * Auto refresh help * Use installed.packages to handle package updates * Only write log when restarting help server Co-authored-by: Kun Ren --- R/help/helpServer.R | 15 +++- src/helpViewer/helpProvider.ts | 160 ++++++++++++++------------------- src/helpViewer/index.ts | 15 +++- src/helpViewer/packages.ts | 4 +- src/helpViewer/treeView.ts | 12 ++- 5 files changed, 101 insertions(+), 105 deletions(-) diff --git a/R/help/helpServer.R b/R/help/helpServer.R index 7b6d264b5..633712b91 100644 --- a/R/help/helpServer.R +++ b/R/help/helpServer.R @@ -1,6 +1,8 @@ # get values from extension-set env values lim <- Sys.getenv("VSCR_LIM") +NEW_PACKAGE_STRING <- "NEW_PACKAGES" + cat( lim, tools::startDynamicHelp(), @@ -8,4 +10,15 @@ cat( 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) +} diff --git a/src/helpViewer/helpProvider.ts b/src/helpViewer/helpProvider.ts index 12f9888e3..a0f1cac9f 100644 --- a/src/helpViewer/helpProvider.ts +++ b/src/helpViewer/helpProvider.ts @@ -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 @@ -23,10 +25,12 @@ export class HelpProvider { private port: number|Promise; 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! } @@ -37,7 +41,9 @@ export class HelpProvider { public async launchRHelpServer(): Promise{ 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'); @@ -53,7 +59,7 @@ export class HelpProvider { let str = ''; // promise containing the first output of the r process (contains only the port number) - const outputPromise = new Promise((resolve) => { + const portPromise = new Promise((resolve) => { this.cp.stdout?.on('data', (data) => { try{ // eslint-disable-next-line @@ -61,8 +67,13 @@ export class HelpProvider { } 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', () => { @@ -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 { + public async getHelpFileFromRequestPath(requestPath: string): Promise { // 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 '/' @@ -174,6 +184,9 @@ interface PackageAliases { [key: string]: string; } } +interface AllPackageAliases { + [key: string]: PackageAliases +} // Implements the aliasProvider required by the help panel export class AliasProvider { @@ -181,10 +194,7 @@ 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){ @@ -195,114 +205,78 @@ 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 { + 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('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); @@ -310,8 +284,6 @@ export class AliasProvider { } finally { fs.rmdirSync(tempDir, {recursive: true}); } - // update persistent workspace cache - void this.persistentState?.update('r.helpPanel.cachedPackageAliases', this.allPackageAliases); + return allPackageAliases; } } - diff --git a/src/helpViewer/index.ts b/src/helpViewer/index.ts index d3e0d4a13..4c146b0cf 100644 --- a/src/helpViewer/index.ts +++ b/src/helpViewer/index.ts @@ -174,7 +174,11 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer { + 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); @@ -207,11 +211,14 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer { 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; } diff --git a/src/helpViewer/packages.ts b/src/helpViewer/packages.ts index 2443c58be..86b15cdc1 100644 --- a/src/helpViewer/packages.ts +++ b/src/helpViewer/packages.ts @@ -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 { + await this.clearCachedFiles(); this.cranUrl = undefined; this.pullFavoriteNames(); - void this.clearCachedFiles(); } // Funciton to clear only the cached files regarding an individual package etc. diff --git a/src/helpViewer/treeView.ts b/src/helpViewer/treeView.ts index 98ea8eac1..02ab9764c 100644 --- a/src/helpViewer/treeView.ts +++ b/src/helpViewer/treeView.ts @@ -74,6 +74,10 @@ export class HelpTreeWrapper { listener(node); } } + + public refreshPackageRootNode(): void { + this.helpViewProvider.rootItem?.pkgRootNode?.refresh(); + } } @@ -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); @@ -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; // Shows a quickpick containing the children of a node // If the picked child has children itself, another quickpick is shown @@ -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(); } } From 0845351e76866d192a5ac3905e585e3bafb1baa4 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Sat, 13 Nov 2021 17:42:34 +0800 Subject: [PATCH 56/60] Handle plot on attach (#852) --- R/session/vsc.R | 7 ++----- src/liveShare/shareSession.ts | 10 ++++------ src/session.ts | 7 ++++--- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/R/session/vsc.R b/R/session/vsc.R index 8943e61cb..c2f0695eb 100644 --- a/R/session/vsc.R +++ b/R/session/vsc.R @@ -480,16 +480,13 @@ attach <- function() { request("attach", version = sprintf("%s.%s", R.version$major, R.version$minor), tempdir = tempdir, - plot = getOption("vsc.plot", "Two"), info = list( command = commandArgs()[[1L]], version = R.version.string, start_time = format(file.info(tempdir)$ctime) - ) + ), + plot_url = if (identical(names(dev.cur()), "httpgd")) httpgd::hgd_url() ) - if (identical(names(dev.cur()), "httpgd")) { - .vsc$request("httpgd", url = httpgd::hgd_url()) - } } path_to_uri <- function(path) { diff --git a/src/liveShare/shareSession.ts b/src/liveShare/shareSession.ts index 9a7e1bc0e..510df1e0f 100644 --- a/src/liveShare/shareSession.ts +++ b/src/liveShare/shareSession.ts @@ -11,7 +11,6 @@ import { docProvider, docScheme } from './virtualDocs'; // Workspace Vars let guestPid: string; -let guestPlotView: string; export let guestGlobalenv: unknown; export let guestResDir: string; let rVer: string; @@ -103,7 +102,6 @@ export async function updateGuestRequest(file: string, force: boolean = false): } case 'attach': { guestPid = String(request.pid); - guestPlotView = String(request.plot); console.info(`[updateGuestRequest] attach PID: ${guestPid}`); sessionStatusBarItem.text = `Guest R ${rVer}: ${guestPid}`; sessionStatusBarItem.tooltip = `${info.version}\nProcess ID: ${guestPid}\nCommand: ${info.command}\nStart time: ${info.start_time}\nClick to attach to host terminal.`; @@ -133,7 +131,6 @@ export async function updateGuestRequest(file: string, force: boolean = false): } } else { guestPid = String(request.pid); - guestPlotView = String(request.plot); rVer = String(request.version); info = request.info; @@ -159,16 +156,17 @@ export function updateGuestGlobalenv(hostEnv: string): void { let panel: vscode.WebviewPanel = undefined; export async function updateGuestPlot(file: string): Promise { const plotContent = await readContent(file, 'base64'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const guestPlotView: vscode.ViewColumn = vscode.ViewColumn[config().get('session.viewers.viewColumn.plot')]; if (plotContent) { if (panel) { panel.webview.html = getGuestImageHtml(plotContent); - panel.reveal(vscode.ViewColumn[guestPlotView], true); + panel.reveal(guestPlotView, true); } else { panel = vscode.window.createWebviewPanel('dataview', 'R Guest Plot', { preserveFocus: true, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - viewColumn: vscode.ViewColumn[guestPlotView], + viewColumn: guestPlotView, }, { enableScripts: true, diff --git a/src/session.ts b/src/session.ts index dd97f2b43..4275f6c22 100644 --- a/src/session.ts +++ b/src/session.ts @@ -31,7 +31,6 @@ let info: any; export let globalenvFile: string; let globalenvLockFile: string; let globalenvTimeStamp: number; -let plotView: string; let plotFile: string; let plotLockFile: string; let plotTimeStamp: number; @@ -163,7 +162,7 @@ async function updatePlot() { void commands.executeCommand('vscode.open', Uri.file(plotFile), { preserveFocus: true, preview: true, - viewColumn: ViewColumn[plotView], + viewColumn: ViewColumn[config().get('session.viewers.viewColumn.plot')], }); console.info('[updatePlot] Done'); if (isLiveShare()) { @@ -740,13 +739,15 @@ async function updateRequest(sessionStatusBarItem: StatusBarItem) { info = request.info; sessionDir = path.join(request.tempdir, 'vscode-R'); workingDir = request.wd; - plotView = String(request.plot); console.info(`[updateRequest] attach PID: ${pid}`); sessionStatusBarItem.text = `R ${rVer}: ${pid}`; sessionStatusBarItem.tooltip = `${info.version}\nProcess ID: ${pid}\nCommand: ${info.command}\nStart time: ${info.start_time}\nClick to attach to active terminal.`; sessionStatusBarItem.show(); updateSessionWatcher(); purgeAddinPickerItems(); + if (request.plot_url) { + globalHttpgdManager?.showViewer(request.plot_url); + } break; } case 'browser': { From 49806ecb69760d74e53d752c79854367b70edea0 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Fri, 19 Nov 2021 14:11:15 +1100 Subject: [PATCH 57/60] Initial handling of xarningan::inf_mr --- src/rmarkdown/knit.ts | 6 ++++++ src/rmarkdown/manager.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index 3bf0957a3..baccf5898 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -7,6 +7,7 @@ import yaml = require('js-yaml'); import { RMarkdownManager, KnitWorkingDirectory, DisposableProcess } from './manager'; import { runTextInTerm } from '../rTerminal'; import { extensionContext, rmdPreviewManager } from '../extension'; +import { showBrowser } from '../session'; export let knitDir: KnitWorkingDirectory = util.config().get('rmarkdown.knit.defaults.knitWorkingDirectory') ?? undefined; @@ -35,6 +36,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { const lim = '---vsc---'; const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'gms'); + const serveReg = /(http:\/\/127[0-9.:]*\/.*\.html)/g; const scriptValues = { 'VSCR_KNIT_DIR': knitWorkingDirText, 'VSCR_LIM': lim, @@ -43,6 +45,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { const callback = (dat: string) => { const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); + const serveUrl = serveReg.exec(dat)?.[0]?.replace(serveReg, '$1'); if (outputUrl) { if (openOutfile) { const outFile = vscode.Uri.file(outputUrl); @@ -53,6 +56,9 @@ export class RMarkdownKnitManager extends RMarkdownManager { } } return true; + } else if (serveUrl) { + void showBrowser(serveUrl, docName, 'Beside'); + return true; } else { return false; } diff --git a/src/rmarkdown/manager.ts b/src/rmarkdown/manager.ts index 490eb039c..ae23088e2 100644 --- a/src/rmarkdown/manager.ts +++ b/src/rmarkdown/manager.ts @@ -151,6 +151,11 @@ export abstract class RMarkdownManager { if (printOutput) { this.rMarkdownOutput.appendLine(dat); } + // for some reason, some knitting formats have the output + // of the file url in the stderr stream + if (args.callback(dat, childProcess)) { + resolve(childProcess); + } }); childProcess.on('exit', (code, signal) => { From 60da38a9cdd6c88964d1bac9c2ffc8a3c67abc73 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 22 Nov 2021 14:34:39 +1100 Subject: [PATCH 58/60] Kill the child process! Now stops the child process when the webview is closed --- src/rmarkdown/knit.ts | 11 +++++++++-- src/session.ts | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index baccf5898..efab93d69 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -43,7 +43,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { 'VSCR_KNIT_COMMAND': knitCommand }; - const callback = (dat: string) => { + const callback = (dat: string, process: DisposableProcess) => { const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); const serveUrl = serveReg.exec(dat)?.[0]?.replace(serveReg, '$1'); if (outputUrl) { @@ -57,7 +57,14 @@ export class RMarkdownKnitManager extends RMarkdownManager { } return true; } else if (serveUrl) { - void showBrowser(serveUrl, docName, 'Beside'); + const browser = showBrowser(serveUrl, docName, 'Beside'); + void browser.then((value) => { + if (value) { + value.onDidDispose(() => { + process.kill('SIGTERM'); + }); + } + }); return true; } else { return false; diff --git a/src/session.ts b/src/session.ts index 4275f6c22..32c78f525 100644 --- a/src/session.ts +++ b/src/session.ts @@ -195,7 +195,7 @@ async function updateGlobalenv() { } } -export async function showBrowser(url: string, title: string, viewer: string | boolean): Promise { +export async function showBrowser(url: string, title: string, viewer: string | boolean): Promise { console.info(`[showBrowser] uri: ${url}, viewer: ${viewer.toString()}`); const uri = Uri.parse(url); if (viewer === false) { @@ -240,6 +240,7 @@ export async function showBrowser(url: string, title: string, viewer: string | b }); panel.iconPath = new UriIcon('globe'); panel.webview.html = getBrowserHtml(externalUri); + return (panel); } console.info('[showBrowser] Done'); } From 4ebe16a0bb91eae311392d96d7a0dacee44dd8aa Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 2 Dec 2021 15:54:22 +1100 Subject: [PATCH 59/60] Allow localhost --- src/rmarkdown/knit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index efab93d69..e88ecf67a 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -36,7 +36,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { const lim = '---vsc---'; const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'gms'); - const serveReg = /(http:\/\/127[0-9.:]*\/.*\.html)/g; + const serveReg = /(http:\/\/(localhost)?[0-9.:]*\/.*\.html)/g; const scriptValues = { 'VSCR_KNIT_DIR': knitWorkingDirText, 'VSCR_LIM': lim, From da0b18b5de01e4772f31f84f0b9c1bdf2b46131b Mon Sep 17 00:00:00 2001 From: ElianHugh Date: Thu, 12 May 2022 19:10:40 +1000 Subject: [PATCH 60/60] Force file to be self contained --- R/rmarkdown/preview.R | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/R/rmarkdown/preview.R b/R/rmarkdown/preview.R index 87bdea362..a0ed7771d 100644 --- a/R/rmarkdown/preview.R +++ b/R/rmarkdown/preview.R @@ -27,6 +27,7 @@ set_html <- tryCatch( } ) + # set the knitr chunk eval directory # mainly affects source calls knitr::opts_knit[["set"]](root.dir = knit_dir) @@ -35,10 +36,12 @@ knitr::opts_knit[["set"]](root.dir = knit_dir) cat( lim, rmarkdown::render( - file_path, + input = file_path, output_format = set_html, output_file = output_file_loc, - intermediates_dir = tmp_dir + output_dir = tmp_dir, + intermediates_dir = tmp_dir, + output_options = list(self_contained = TRUE) ), lim, sep = ""