From 20e5096b145e57a9514383794eee00865a6cbe67 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 1 Jul 2021 13:59:37 +1000 Subject: [PATCH 01/21] Initial rmd preview implementation - Preview rmd w/o blocking the terminal --- package.json | 61 ++++++++++++ src/extension.ts | 8 +- src/{rmarkdown.ts => rmarkdown/index.ts} | 4 +- src/rmarkdown/preview.ts | 118 +++++++++++++++++++++++ src/session.ts | 2 +- 5 files changed, 188 insertions(+), 5 deletions(-) rename src/{rmarkdown.ts => rmarkdown/index.ts} (99%) create mode 100644 src/rmarkdown/preview.ts diff --git a/package.json b/package.json index e9a339a9d..cc7f6b97b 100644 --- a/package.json +++ b/package.json @@ -526,6 +526,23 @@ "category": "R", "command": "r.goToNextChunk" }, + { + "command": "r.markdown.previewSide", + "title": "Open preview to the side", + "icon": "$(open-preview)", + "category": "R" + }, + { + "command": "r.markdown.preview", + "title": "Open preview", + "icon": "$(open-preview)", + "category": "R" + }, + { + "command": "r.markdown.explorerPreview", + "title": "Open preview", + "category": "R" + }, { "title": "Launch RStudio Addin", "category": "R", @@ -797,6 +814,18 @@ "key": "Ctrl+alt+e", "mac": "cmd+alt+e", "when": "editorTextFocus && editorLangId == 'r'" + }, + { + "command": "r.markdown.previewSide", + "key": "Ctrl+k v", + "mac": "cmd+k v", + "when": "editorTextFocus && editorLangId == 'rmd'" + }, + { + "command": "r.markdown.preview", + "key": "Ctrl+shift+v", + "mac": "cmd+shift+v", + "when": "editorTextFocus && editorLangId == 'rmd'" } ], "menus": { @@ -906,6 +935,12 @@ "group": "httpgd", "when": "resourceScheme =~ /webview/ && r.plot.active", "command": "r.plot.openUrl" + }, + { + "command": "r.markdown.previewSide", + "alt": "r.markdown.preview", + "when": "editorLangId == rmd", + "group": "navigation" } ], "editor/context": [ @@ -1040,6 +1075,19 @@ "group": "navigation@3", "when": "view == rLiveShare && r.liveShare:aborted" } + ], + "explorer/context": [ + { + "command": "r.markdown.explorerPreview", + "when": "resourceLangId == rmd", + "group": "navigation" + } + ], + "commandPalette": [ + { + "command": "r.markdown.explorerPreview", + "when": "false" + } ] }, "configuration": { @@ -1108,6 +1156,19 @@ "default": "rgba(128, 128, 128, 0.1)", "description": "RMarkdown chunk background color in RGBA or RGB value. Defaults to rgba(128, 128, 128, 0.1). Leave it empty to disable it (use default editor background color). Reload VS Code after changing settings.\n\nLearn how to set colors: https://www.w3schools.com/css/css_colors_rgb.asp.\n\nExamples for syntax rgba(, , , ):\nrgba(128, 128, 128, 0.1)\nrgba(128, 128, 128, 0.3)\nrgba(255, 165, 0, 0.1)\n\n" }, + "r.rmarkdown.previewEngine": { + "type": "string", + "enum": [ + "rmarkdown::run", + "xaringan::infinite_moon_reader" + ], + "enumDescriptions": [ + "Use rmarkdown::run() to preview documents", + "Use xaringan::infinite_moon_reader() to preview documents" + ], + "description": "What package command to use for previewing R Markdown documents.", + "default": "rmarkdown::run" + }, "r.helpPanel.enableSyntaxHighlighting": { "type": "boolean", "default": true, diff --git a/src/extension.ts b/src/extension.ts index eb373503b..39fe9414a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,6 +19,7 @@ import * as completions from './completions'; import * as rShare from './liveshare'; import * as httpgdViewer from './plotViewer'; +import { PreviewProvider } from './rmarkdown/preview'; // global objects used in other files export let rWorkspace: workspaceViewer.WorkspaceDataProvider | undefined = undefined; @@ -26,7 +27,7 @@ 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 const rPreviewProvider = new PreviewProvider(); // Called (once) when the extension is activated export async function activate(context: vscode.ExtensionContext): Promise { @@ -79,6 +80,10 @@ export async function activate(context: vscode.ExtensionContext): Promise rPreviewProvider.previewRmd(viewer), + 'r.markdown.preview': (viewer = vscode.ViewColumn.Active) => rPreviewProvider.previewRmd(viewer), + 'r.markdown.explorerPreview': (uri: vscode.Uri, viewer = vscode.ViewColumn.Active) => rPreviewProvider.previewRmd(viewer, uri), + // editor independent commands 'r.createGitignore': rGitignore.createGitignore, 'r.loadAll': () => rTerminal.runTextInTerm('devtools::load_all()'), @@ -136,7 +141,6 @@ export async function activate(context: vscode.ExtensionContext): Promise e.file === fileName)) { + this.openProcesses.filter(e => e.file === fileName)[0].panel.reveal(); + } else { + try { + call = cp.spawn(cmd, null, { shell: true }); + } catch (e) { + console.error((e as unknown).toString()); + } + + (call as cp.ChildProcessWithoutNullStreams).stderr.on('data', + (data: Buffer) => { + const dat = data.toString('utf8'); + const match = reg.exec(dat)?.[0]; + const previewUrl = previewEngine === 'rmarkdown::run' ? `http://${match}/${fileName}` : `http://${match}.html`; + if (match) { + void this.showPreview(previewUrl, fileName, call, viewer); + } + }); + } + } + + private async showPreview(url: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn): Promise { + console.info(`[showPreview] uri: ${url}`); + const uri = vscode.Uri.parse(url); + const externalUri = await vscode.env.asExternalUri(uri); + const panel = vscode.window.createWebviewPanel( + 'previewRmd', + `Preview ${title}`, + { + preserveFocus: true, + viewColumn: viewer + }, + { + enableFindWidget: true, + enableScripts: true, + retainContextWhenHidden: true, + }); + + this.openProcesses.push( + { + cp: cp, + file: title, + panel: panel, + externalUri: externalUri, + browserUri: uri + } + ); + + if (isHost()) { + await shareBrowser(url, title); + } + + // destroy process on closing window + panel.onDidDispose(() => { + if (isHost()) { + closeBrowser(url); + } + kill(cp.pid); + for (const [key, item] of this.openProcesses.entries()) { + if (item.file === title) { + this.openProcesses.splice(key); + } + } + }); + + panel.webview.html = getBrowserHtml(externalUri); + } + +} diff --git a/src/session.ts b/src/session.ts index 6095b1e72..658b9ba0f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -238,7 +238,7 @@ export async function showBrowser(url: string, title: string, viewer: string | b console.info('[showBrowser] Done'); } -function getBrowserHtml(uri: Uri) { +export function getBrowserHtml(uri: Uri) { return ` From 7ec5c65a8f2d3e2451052b4f1d79960d4a944959 Mon Sep 17 00:00:00 2001 From: ElianHugh Date: Thu, 1 Jul 2021 21:17:18 +1000 Subject: [PATCH 02/21] Add custom context for previews --- package.json | 5 +++++ src/rmarkdown/preview.ts | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index cc7f6b97b..2fb87d411 100644 --- a/package.json +++ b/package.json @@ -543,6 +543,11 @@ "title": "Open preview", "category": "R" }, + { + "command": "r.markdown.refresh", + "title": "Refresh preview", + "category": "R" + }, { "title": "Launch RStudio Addin", "category": "R", diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 3b6c157e6..819743f47 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -101,18 +101,29 @@ export class PreviewProvider { // destroy process on closing window panel.onDidDispose(() => { - if (isHost()) { - closeBrowser(url); - } + void vscode.commands.executeCommand('setContext', 'r.preview.active', false); kill(cp.pid); for (const [key, item] of this.openProcesses.entries()) { if (item.file === title) { - this.openProcesses.splice(key); + this.openProcesses.splice(key, 1); } } + if (isHost()) { + closeBrowser(url); + } + }); + + panel.onDidChangeViewState(({ webviewPanel }) => { + void vscode.commands.executeCommand('setContext', 'r.preview.active', webviewPanel.active); }); panel.webview.html = getBrowserHtml(externalUri); } + public refreshPanel(panel: vscode.WebviewPanel): void { + const refreshPanel = this.openProcesses.filter(e => e.panel === panel)[0]; + refreshPanel.panel.webview.html = ''; + refreshPanel.panel.webview.html = getBrowserHtml(refreshPanel.externalUri); + } + } From 7a7559d300fb1fe7e3a0b1839f389a70f447b005 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 1 Jul 2021 22:36:49 +1000 Subject: [PATCH 03/21] Show source, refresh, and open external for rmd previews --- package.json | 28 +++++++++++++++++++++++ src/extension.ts | 3 +++ src/rmarkdown/preview.ts | 48 +++++++++++++++++++++++++++++++--------- 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 2fb87d411..c87fd819b 100644 --- a/package.json +++ b/package.json @@ -546,6 +546,19 @@ { "command": "r.markdown.refresh", "title": "Refresh preview", + "icon": "$(refresh)", + "category": "R" + }, + { + "command": "r.markdown.openExternal", + "title": "Open in external browser", + "icon": "$(link-external)", + "category": "R" + }, + { + "command": "r.markdown.showSource", + "title": "Show source", + "icon": "$(go-to-file)", "category": "R" }, { @@ -846,6 +859,21 @@ } ], "editor/title": [ + { + "command": "r.markdown.refresh", + "when": "resourceScheme =~ /webview/ && r.preview.active", + "group": "navigation@3" + }, + { + "command": "r.markdown.openExternal", + "when": "resourceScheme =~ /webview/ && r.preview.active", + "group": "navigation@1" + }, + { + "command": "r.markdown.showSource", + "when": "resourceScheme =~ /webview/ && r.preview.active", + "group": "navigation@2" + }, { "command": "r.browser.refresh", "when": "resourceScheme =~ /webview/ && r.browser.active", diff --git a/src/extension.ts b/src/extension.ts index 39fe9414a..a434b1732 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -83,6 +83,9 @@ export async function activate(context: vscode.ExtensionContext): Promise rPreviewProvider.previewRmd(viewer), 'r.markdown.preview': (viewer = vscode.ViewColumn.Active) => rPreviewProvider.previewRmd(viewer), 'r.markdown.explorerPreview': (uri: vscode.Uri, viewer = vscode.ViewColumn.Active) => rPreviewProvider.previewRmd(viewer, uri), + 'r.markdown.refresh': () => rPreviewProvider.refreshPanel(), + 'r.markdown.openExternal': () => rPreviewProvider.openExternal(), + 'r.markdown.showSource': () => rPreviewProvider.showSource(), // editor independent commands 'r.createGitignore': rGitignore.createGitignore, diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 819743f47..78d85cd81 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -17,6 +17,10 @@ interface IPreviewProcess { export class PreviewProvider { private openProcesses: IPreviewProcess[] = []; + private activePreview: vscode.WebviewPanel; + private activeResource: vscode.Uri; + private activeExternalResource: vscode.Uri; + public previewRmd(viewer: vscode.ViewColumn, uri?: vscode.Uri): void { const fileUri = uri ?? vscode.window.activeTextEditor.document.uri; const fileName = fileUri.path.substring(fileUri.path.lastIndexOf('/') + 1); @@ -62,13 +66,36 @@ export class PreviewProvider { const match = reg.exec(dat)?.[0]; const previewUrl = previewEngine === 'rmarkdown::run' ? `http://${match}/${fileName}` : `http://${match}.html`; if (match) { - void this.showPreview(previewUrl, fileName, call, viewer); + void this.showPreview(previewUrl, fileName, call, viewer, fileUri); } }); } } - private async showPreview(url: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn): Promise { + public refreshPanel(): void { + if (this.activePreview) { + this.activePreview.webview.html = ''; + this.activePreview.webview.html = getBrowserHtml(this.activeExternalResource); + } + } + + public async showSource(): Promise { + if (this.activeResource) { + await vscode.commands.executeCommand('vscode.open', this.activeResource, { + preserveFocus: false, + preview: false, + viewColumn: vscode.ViewColumn.Active + }); + } + } + + public openExternal(): void { + if (this.activeExternalResource) { + void vscode.env.openExternal(this.activeExternalResource); + } + } + + private async showPreview(url: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, fileUri: vscode.Uri): Promise { console.info(`[showPreview] uri: ${url}`); const uri = vscode.Uri.parse(url); const externalUri = await vscode.env.asExternalUri(uri); @@ -99,15 +126,17 @@ export class PreviewProvider { await shareBrowser(url, title); } - // destroy process on closing window panel.onDidDispose(() => { - void vscode.commands.executeCommand('setContext', 'r.preview.active', false); + // destroy process on closing window kill(cp.pid); + + void vscode.commands.executeCommand('setContext', 'r.preview.active', false); for (const [key, item] of this.openProcesses.entries()) { if (item.file === title) { this.openProcesses.splice(key, 1); } } + if (isHost()) { closeBrowser(url); } @@ -115,15 +144,14 @@ export class PreviewProvider { panel.onDidChangeViewState(({ webviewPanel }) => { void vscode.commands.executeCommand('setContext', 'r.preview.active', webviewPanel.active); + if (webviewPanel.active) { + this.activePreview = webviewPanel; + this.activeResource = fileUri; + this.activeExternalResource = externalUri; + } }); panel.webview.html = getBrowserHtml(externalUri); } - public refreshPanel(panel: vscode.WebviewPanel): void { - const refreshPanel = this.openProcesses.filter(e => e.panel === panel)[0]; - refreshPanel.panel.webview.html = ''; - refreshPanel.panel.webview.html = getBrowserHtml(refreshPanel.externalUri); - } - } From 2bd4d535f60daf2e600609c5ca16065bb1b723d4 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 1 Jul 2021 23:50:13 +1000 Subject: [PATCH 04/21] Refactor, bug fix view column, capitalisation - Fixed a bug that meant the side preview was incorrectly opening in the active column - Fixed a bug that incorrectly caused show source to open the source doc in a new column --- package.json | 12 ++++++------ src/extension.ts | 6 +++--- src/rmarkdown/preview.ts | 9 +++------ 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index c87fd819b..8b9074a83 100644 --- a/package.json +++ b/package.json @@ -528,36 +528,36 @@ }, { "command": "r.markdown.previewSide", - "title": "Open preview to the side", + "title": "Open Preview to the Side", "icon": "$(open-preview)", "category": "R" }, { "command": "r.markdown.preview", - "title": "Open preview", + "title": "Open Preview", "icon": "$(open-preview)", "category": "R" }, { "command": "r.markdown.explorerPreview", - "title": "Open preview", + "title": "Open Preview", "category": "R" }, { "command": "r.markdown.refresh", - "title": "Refresh preview", + "title": "Refresh Preview", "icon": "$(refresh)", "category": "R" }, { "command": "r.markdown.openExternal", - "title": "Open in external browser", + "title": "Open in External Browser", "icon": "$(link-external)", "category": "R" }, { "command": "r.markdown.showSource", - "title": "Show source", + "title": "Show Source", "icon": "$(go-to-file)", "category": "R" }, diff --git a/src/extension.ts b/src/extension.ts index a434b1732..fbd3c1d78 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -80,9 +80,9 @@ export async function activate(context: vscode.ExtensionContext): Promise rPreviewProvider.previewRmd(viewer), - 'r.markdown.preview': (viewer = vscode.ViewColumn.Active) => rPreviewProvider.previewRmd(viewer), - 'r.markdown.explorerPreview': (uri: vscode.Uri, viewer = vscode.ViewColumn.Active) => rPreviewProvider.previewRmd(viewer, uri), + 'r.markdown.previewSide': () => rPreviewProvider.previewRmd(vscode.ViewColumn.Beside, null), + 'r.markdown.preview': () => rPreviewProvider.previewRmd(vscode.ViewColumn.Active, null), + 'r.markdown.explorerPreview': (uri: vscode.Uri) => rPreviewProvider.previewRmd(vscode.ViewColumn.Active, uri), 'r.markdown.refresh': () => rPreviewProvider.refreshPanel(), 'r.markdown.openExternal': () => rPreviewProvider.openExternal(), 'r.markdown.showSource': () => rPreviewProvider.showSource(), diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 78d85cd81..6920fabb9 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -11,8 +11,6 @@ interface IPreviewProcess { cp: cp.ChildProcessWithoutNullStreams, file: string, panel: vscode.WebviewPanel, - externalUri: vscode.Uri, - browserUri: vscode.Uri } export class PreviewProvider { @@ -81,10 +79,11 @@ export class PreviewProvider { public async showSource(): Promise { if (this.activeResource) { + const viewCol = vscode.window.visibleTextEditors.filter(e => e.document.uri === this.activeResource)[0]?.viewColumn; await vscode.commands.executeCommand('vscode.open', this.activeResource, { preserveFocus: false, preview: false, - viewColumn: vscode.ViewColumn.Active + viewColumn: viewCol ?? vscode.ViewColumn.Active }); } } @@ -116,9 +115,7 @@ export class PreviewProvider { { cp: cp, file: title, - panel: panel, - externalUri: externalUri, - browserUri: uri + panel: panel } ); From 56310627250be28a17a6fa3bb99ba3358cb3026a Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Fri, 2 Jul 2021 21:34:16 +1000 Subject: [PATCH 05/21] Bugfix inf_mr, R to rPath, progress notification --- src/extension.ts | 6 ++- src/rmarkdown/preview.ts | 109 ++++++++++++++++++++++++--------------- src/util.ts | 5 +- 3 files changed, 75 insertions(+), 45 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index fbd3c1d78..ddf6aa712 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,7 +27,7 @@ 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 const rPreviewProvider = new PreviewProvider(); +export let rPreviewProvider: PreviewProvider | undefined = undefined; // Called (once) when the extension is activated export async function activate(context: vscode.ExtensionContext): Promise { @@ -144,6 +144,10 @@ export async function activate(context: vscode.ExtensionContext): Promise { + this.rPath = await getRpath(false); + } - public previewRmd(viewer: vscode.ViewColumn, uri?: vscode.Uri): void { + public async previewRmd(viewer: vscode.ViewColumn, uri?: vscode.Uri): Promise { const fileUri = uri ?? vscode.window.activeTextEditor.document.uri; const fileName = fileUri.path.substring(fileUri.path.lastIndexOf('/') + 1); const previewEngine: string = config().get('rmarkdown.previewEngine'); const cmd = ( - `R --silent --slave --no-save --no-restore -e "${previewEngine}('${fileUri.path}')"` + `${this.rPath} --silent --slave --no-save --no-restore -e "${previewEngine}('${fileUri.path}')"` ); - - let reg: RegExp = undefined; + const reg: RegExp = this.constructRegex(previewEngine); let call = undefined; - // the regex can be extended in the future for other - // calls - switch (previewEngine) { - // the rmarkdown::run url is of the structure: - // http://127.0.0.1:port/file.Rmd - case 'rmarkdown::run': { - reg = /(?<=http:\/\/)[0-9.:]*/g; - break; - } - // the inf_mr output url is of the structure: - // http://127.0.0.1:port/path/to/file.html - case 'xaringan::inf_mr' || 'xaringan::infinite_moon_reader': { - reg = /(?<=http:\/\/)(.*)(?=\.html)/g; - break; - } - default: break; - } - if (this.openProcesses.some(e => e.file === fileName)) { this.openProcesses.filter(e => e.file === fileName)[0].panel.reveal(); } else { - try { - call = cp.spawn(cmd, null, { shell: true }); - } catch (e) { - console.error((e as unknown).toString()); - } - - (call as cp.ChildProcessWithoutNullStreams).stderr.on('data', - (data: Buffer) => { - const dat = data.toString('utf8'); - const match = reg.exec(dat)?.[0]; - const previewUrl = previewEngine === 'rmarkdown::run' ? `http://${match}/${fileName}` : `http://${match}.html`; - if (match) { - void this.showPreview(previewUrl, fileName, call, viewer, fileUri); - } - }); + await doWithProgress(() => { + try { + call = cp.spawn(cmd, null, { shell: true }); + } catch (e) { + console.warn((e as string)); + } + (call as cp.ChildProcessWithoutNullStreams).stderr.on('data', + (data: Buffer) => { + const dat = data.toString('utf8'); + const match = reg.exec(dat)?.[0]; + const previewUrl = this.constructUrl(previewEngine, match, fileName); + if (match) { + void this.showPreview(previewUrl, fileName, call, viewer, fileUri); + } + }); + }, + vscode.ProgressLocation.Notification, + `Rendering ${fileName}...` + ); } } @@ -95,6 +84,7 @@ export class PreviewProvider { } private async showPreview(url: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, fileUri: vscode.Uri): Promise { + // construct webview and its related html console.info(`[showPreview] uri: ${url}`); const uri = vscode.Uri.parse(url); const externalUri = await vscode.env.asExternalUri(uri); @@ -110,7 +100,10 @@ export class PreviewProvider { enableScripts: true, retainContextWhenHidden: true, }); + panel.webview.html = getBrowserHtml(externalUri); + // Push the new rmd webview to the open proccesses array, + // to keep track of running child processes this.openProcesses.push( { cp: cp, @@ -123,11 +116,12 @@ export class PreviewProvider { await shareBrowser(url, title); } + // state change panel.onDidDispose(() => { // destroy process on closing window kill(cp.pid); - void vscode.commands.executeCommand('setContext', 'r.preview.active', false); + void setContext('r.preview.active', false); for (const [key, item] of this.openProcesses.entries()) { if (item.file === title) { this.openProcesses.splice(key, 1); @@ -140,15 +134,46 @@ export class PreviewProvider { }); panel.onDidChangeViewState(({ webviewPanel }) => { - void vscode.commands.executeCommand('setContext', 'r.preview.active', webviewPanel.active); + void setContext('r.preview.active', webviewPanel.active); if (webviewPanel.active) { this.activePreview = webviewPanel; this.activeResource = fileUri; this.activeExternalResource = externalUri; } }); + } - panel.webview.html = getBrowserHtml(externalUri); + private constructUrl(previewEngine: string, match: string, fileName?: string): string { + switch (previewEngine) { + case 'rmarkdown::run': { + return `http://${match}/${fileName}`; + } + case 'xaringan::infinite_moon_reader': { + return `http://${match}.html`; + } + default: { + console.error(`[PreviewProvider] unsupported preview engine supplied as argument: ${previewEngine}`); + break; + } + } } + private constructRegex(previewEngine: string): RegExp { + switch (previewEngine) { + // the rmarkdown::run url is of the structure: + // http://127.0.0.1:port/file.Rmd + case 'rmarkdown::run': { + return /(?<=http:\/\/)[0-9.:]*/g; + } + // the inf_mr output url is of the structure: + // http://127.0.0.1:port/path/to/file.html + case 'xaringan::infinite_moon_reader': { + return /(?<=http:\/\/)(.*)(?=\.html)/g; + } + default: { + console.error(`[PreviewProvider] unsupported preview engine supplied as argument: ${previewEngine}`); + break; + } + } + } } diff --git a/src/util.ts b/src/util.ts index 502a47a5f..693fba19d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -230,11 +230,12 @@ 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: () => T | Promise, location: string | vscode.ProgressLocation = 'rHelpPages'): Promise { +export async function doWithProgress(cb: () => T | Promise, location: string | vscode.ProgressLocation = 'rHelpPages', title?: string): Promise { const location2 = (typeof location === 'string' ? { viewId: location } : location); const options: vscode.ProgressOptions = { location: location2, - cancellable: false + cancellable: false, + title: title }; let ret: T; await vscode.window.withProgress(options, async () => { From 2dd2410e0de8a2492eb1b844fef36ae2891f4951 Mon Sep 17 00:00:00 2001 From: ElianHugh Date: Fri, 2 Jul 2021 23:00:00 +1000 Subject: [PATCH 06/21] Rename commands - Rename some commands and the PreviewProvider to be more in line with vscode's markdown preview names --- package.json | 37 +++++++++++++------------------------ src/extension.ts | 19 +++++++++---------- src/rmarkdown/preview.ts | 4 ++-- src/session.ts | 2 +- 4 files changed, 25 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 8b9074a83..0f1dde298 100644 --- a/package.json +++ b/package.json @@ -527,36 +527,31 @@ "command": "r.goToNextChunk" }, { - "command": "r.markdown.previewSide", + "command": "r.markdown.showPreviewToSide", "title": "Open Preview to the Side", "icon": "$(open-preview)", "category": "R" }, { - "command": "r.markdown.preview", + "command": "r.markdown.showPreview", "title": "Open Preview", "icon": "$(open-preview)", "category": "R" }, { - "command": "r.markdown.explorerPreview", - "title": "Open Preview", - "category": "R" - }, - { - "command": "r.markdown.refresh", + "command": "r.markdown.preview.refresh", "title": "Refresh Preview", "icon": "$(refresh)", "category": "R" }, { - "command": "r.markdown.openExternal", + "command": "r.markdown.preview.openExternal", "title": "Open in External Browser", "icon": "$(link-external)", "category": "R" }, { - "command": "r.markdown.showSource", + "command": "r.markdown.preview.showSource", "title": "Show Source", "icon": "$(go-to-file)", "category": "R" @@ -834,13 +829,13 @@ "when": "editorTextFocus && editorLangId == 'r'" }, { - "command": "r.markdown.previewSide", + "command": "r.markdown.showPreviewToSide", "key": "Ctrl+k v", "mac": "cmd+k v", "when": "editorTextFocus && editorLangId == 'rmd'" }, { - "command": "r.markdown.preview", + "command": "r.markdown.showPreview", "key": "Ctrl+shift+v", "mac": "cmd+shift+v", "when": "editorTextFocus && editorLangId == 'rmd'" @@ -860,17 +855,17 @@ ], "editor/title": [ { - "command": "r.markdown.refresh", + "command": "r.markdown.preview.refresh", "when": "resourceScheme =~ /webview/ && r.preview.active", "group": "navigation@3" }, { - "command": "r.markdown.openExternal", + "command": "r.markdown.preview.openExternal", "when": "resourceScheme =~ /webview/ && r.preview.active", "group": "navigation@1" }, { - "command": "r.markdown.showSource", + "command": "r.markdown.preview.showSource", "when": "resourceScheme =~ /webview/ && r.preview.active", "group": "navigation@2" }, @@ -970,8 +965,8 @@ "command": "r.plot.openUrl" }, { - "command": "r.markdown.previewSide", - "alt": "r.markdown.preview", + "command": "r.markdown.showPreviewToSide", + "alt": "r.markdown.showPreview", "when": "editorLangId == rmd", "group": "navigation" } @@ -1111,16 +1106,10 @@ ], "explorer/context": [ { - "command": "r.markdown.explorerPreview", + "command": "r.markdown.showPreview", "when": "resourceLangId == rmd", "group": "navigation" } - ], - "commandPalette": [ - { - "command": "r.markdown.explorerPreview", - "when": "false" - } ] }, "configuration": { diff --git a/src/extension.ts b/src/extension.ts index ddf6aa712..4527d2fb0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,7 +19,7 @@ import * as completions from './completions'; import * as rShare from './liveshare'; import * as httpgdViewer from './plotViewer'; -import { PreviewProvider } from './rmarkdown/preview'; +import { RMarkdownPreviewManager } from './rmarkdown/preview'; // global objects used in other files export let rWorkspace: workspaceViewer.WorkspaceDataProvider | undefined = undefined; @@ -27,7 +27,7 @@ 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 rPreviewProvider: PreviewProvider | undefined = undefined; +export let rMarkdownPreview: RMarkdownPreviewManager | undefined = undefined; // Called (once) when the extension is activated export async function activate(context: vscode.ExtensionContext): Promise { @@ -80,12 +80,11 @@ export async function activate(context: vscode.ExtensionContext): Promise rPreviewProvider.previewRmd(vscode.ViewColumn.Beside, null), - 'r.markdown.preview': () => rPreviewProvider.previewRmd(vscode.ViewColumn.Active, null), - 'r.markdown.explorerPreview': (uri: vscode.Uri) => rPreviewProvider.previewRmd(vscode.ViewColumn.Active, uri), - 'r.markdown.refresh': () => rPreviewProvider.refreshPanel(), - 'r.markdown.openExternal': () => rPreviewProvider.openExternal(), - 'r.markdown.showSource': () => rPreviewProvider.showSource(), + 'r.markdown.showPreviewToSide': () => rMarkdownPreview.previewRmd(vscode.ViewColumn.Beside), + 'r.markdown.showPreview': (uri: vscode.Uri) => rMarkdownPreview.previewRmd(vscode.ViewColumn.Active, uri), + 'r.markdown.preview.refresh': () => rMarkdownPreview.refreshPanel(), + 'r.markdown.preview.openExternal': () => rMarkdownPreview.openExternalBrowser(), + 'r.markdown.preview.showSource': () => rMarkdownPreview.showSource(), // editor independent commands 'r.createGitignore': rGitignore.createGitignore, @@ -145,8 +144,8 @@ export async function activate(context: vscode.ExtensionContext): Promise From d62b82f9a4e2cdf9196af143c66b92d10939cf12 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Sun, 4 Jul 2021 23:43:09 +1000 Subject: [PATCH 07/21] Error messages, output channel, and more - Moved refresh to overflow menu - Change how 'Show Source' retrieves the original column - 'R Markdown' output channel for stderr and stdout - Better track and dispose of child processes - Progress bar now stays for the duration of loading - Now tells the user when the document failed to knit --- package.json | 10 +- src/rmarkdown/preview.ts | 200 ++++++++++++++++++++++++++++----------- 2 files changed, 149 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index 0f1dde298..1b1e23b3e 100644 --- a/package.json +++ b/package.json @@ -854,11 +854,6 @@ } ], "editor/title": [ - { - "command": "r.markdown.preview.refresh", - "when": "resourceScheme =~ /webview/ && r.preview.active", - "group": "navigation@3" - }, { "command": "r.markdown.preview.openExternal", "when": "resourceScheme =~ /webview/ && r.preview.active", @@ -869,6 +864,11 @@ "when": "resourceScheme =~ /webview/ && r.preview.active", "group": "navigation@2" }, + { + "command": "r.markdown.preview.refresh", + "when": "resourceScheme =~ /webview/ && r.preview.active", + "group": "1_rMarkdown" + }, { "command": "r.browser.refresh", "when": "resourceScheme =~ /webview/ && r.browser.active", diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index de733c5b7..7b0ea2ca9 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -1,89 +1,142 @@ import * as cp from 'child_process'; -import * as kill from 'tree-kill'; import * as vscode from 'vscode'; + import { getBrowserHtml } from '../session'; import { closeBrowser, isHost, shareBrowser } from '../liveshare'; import { config, doWithProgress, getRpath, setContext } from '../util'; +import { extensionContext } from '../extension'; + +class RMarkdownChild extends vscode.Disposable { + title: string; + cp: cp.ChildProcessWithoutNullStreams; + panel: vscode.WebviewPanel; + resourceViewColumn: vscode.ViewColumn; + uri: vscode.Uri; + externalUri: vscode.Uri; + + constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, uri: vscode.Uri, externalUri: vscode.Uri) { + super(() => { + this.cp.kill('SIGKILL'); + this.panel?.dispose(); + }); + + this.title = title; + this.cp = cp; + this.panel = panel; + this.resourceViewColumn = resourceViewColumn; + this.uri = uri; + this.externalUri = externalUri; + } +} -interface IPreviewProcess { - cp: cp.ChildProcessWithoutNullStreams, - file: string, - panel: vscode.WebviewPanel, +class RMarkdownChildStore extends vscode.Disposable { + private store: Set = new Set(); + + constructor() { + super((): void => { + for (const child of this.store) { + child.dispose(); + } + this.store.clear(); + }); + } + + public add(child: RMarkdownChild): void { + this.store.add(child); + } + + public delete(child: RMarkdownChild): void { + child.dispose(); + this.store.delete(child); + } + + public get(uri: vscode.Uri) { + for (const child of this.store) { + if (child.uri === uri) { + return child; + } + } + return undefined; + } + + public has(uri: vscode.Uri) { + for (const child of this.store) { + if (child.uri === uri) { + return true; + } + } + return false; + } + + [Symbol.iterator](): Iterator { + return this.store[Symbol.iterator](); + } } export class RMarkdownPreviewManager { - private openProcesses: IPreviewProcess[] = []; - private activePreview: vscode.WebviewPanel; - private activeResource: vscode.Uri; - private activeExternalResource: vscode.Uri; private rPath: string; + private ChildStore: RMarkdownChildStore = new RMarkdownChildStore; + private activePreview: RMarkdownChild; + private rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); public async init(): Promise { this.rPath = await getRpath(false); + extensionContext.subscriptions.push(this.ChildStore); } public async previewRmd(viewer: vscode.ViewColumn, uri?: vscode.Uri): Promise { const fileUri = uri ?? vscode.window.activeTextEditor.document.uri; const fileName = fileUri.path.substring(fileUri.path.lastIndexOf('/') + 1); const previewEngine: string = config().get('rmarkdown.previewEngine'); + const currentViewColumn: vscode.ViewColumn = vscode.window.activeTextEditor.viewColumn ?? vscode.ViewColumn.Active; const cmd = ( `${this.rPath} --silent --slave --no-save --no-restore -e "${previewEngine}('${fileUri.path}')"` ); const reg: RegExp = this.constructRegex(previewEngine); - let call = undefined; - - if (this.openProcesses.some(e => e.file === fileName)) { - this.openProcesses.filter(e => e.file === fileName)[0].panel.reveal(); + if (this.ChildStore.has(fileUri)) { + this.ChildStore.get(fileUri).panel.reveal(); } else { - await doWithProgress(() => { - try { - call = cp.spawn(cmd, null, { shell: true }); - } catch (e) { - console.warn((e as string)); - } - (call as cp.ChildProcessWithoutNullStreams).stderr.on('data', - (data: Buffer) => { - const dat = data.toString('utf8'); - const match = reg.exec(dat)?.[0]; - const previewUrl = this.constructUrl(previewEngine, match, fileName); - if (match) { - void this.showPreview(previewUrl, fileName, call, viewer, fileUri); - } - }); + await doWithProgress(async () => { + await this.spawnProcess(cmd, reg, previewEngine, fileName, viewer, fileUri, currentViewColumn) + .catch((cp: cp.ChildProcessWithoutNullStreams) => { + void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); + cp.kill('SIGKILL'); + } + ); }, vscode.ProgressLocation.Notification, - `Rendering ${fileName}...` + `Knitting ${fileName}...` ); } } public refreshPanel(): void { if (this.activePreview) { - this.activePreview.webview.html = ''; - this.activePreview.webview.html = getBrowserHtml(this.activeExternalResource); + this.activePreview.panel.webview.html = ''; + this.activePreview.panel.webview.html = getBrowserHtml(this.activePreview.uri); } } public async showSource(): Promise { - if (this.activeResource) { - const viewCol = vscode.window.visibleTextEditors.filter(e => e.document.uri === this.activeResource)[0]?.viewColumn; - await vscode.commands.executeCommand('vscode.open', this.activeResource, { + if (this.activePreview) { + // to fix, kind of buggy + await vscode.commands.executeCommand('vscode.open', this.activePreview.uri, { preserveFocus: false, preview: false, - viewColumn: viewCol ?? vscode.ViewColumn.Active + viewColumn: this.activePreview.resourceViewColumn ?? this.activePreview.panel.viewColumn ?? vscode.ViewColumn.Active }); } } public openExternalBrowser(): void { - if (this.activeExternalResource) { - void vscode.env.openExternal(this.activeExternalResource); + if (this.activePreview) { + void vscode.env.openExternal(this.activePreview.externalUri); } } - private async showPreview(url: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, fileUri: vscode.Uri): Promise { + private async showPreview(url: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, fileUri: vscode.Uri, resourceViewColumn: vscode.ViewColumn): Promise { // construct webview and its related html console.info(`[showPreview] uri: ${url}`); const uri = vscode.Uri.parse(url); @@ -104,13 +157,10 @@ export class RMarkdownPreviewManager { // Push the new rmd webview to the open proccesses array, // to keep track of running child processes - this.openProcesses.push( - { - cp: cp, - file: title, - panel: panel - } - ); + // (primarily used in killing the child process, but also + // general state tracking) + const childProcess = new RMarkdownChild(title, cp, panel, resourceViewColumn, fileUri, externalUri); + this.ChildStore.add(childProcess); if (isHost()) { await shareBrowser(url, title); @@ -118,15 +168,10 @@ export class RMarkdownPreviewManager { // state change panel.onDidDispose(() => { - // destroy process on closing window - kill(cp.pid); - + // clear values + this.activePreview === childProcess ? undefined : this.activePreview; void setContext('r.preview.active', false); - for (const [key, item] of this.openProcesses.entries()) { - if (item.file === title) { - this.openProcesses.splice(key, 1); - } - } + this.ChildStore.delete(childProcess); if (isHost()) { closeBrowser(url); @@ -136,9 +181,7 @@ export class RMarkdownPreviewManager { panel.onDidChangeViewState(({ webviewPanel }) => { void setContext('r.preview.active', webviewPanel.active); if (webviewPanel.active) { - this.activePreview = webviewPanel; - this.activeResource = fileUri; - this.activeExternalResource = externalUri; + this.activePreview = childProcess; } }); } @@ -176,4 +219,49 @@ export class RMarkdownPreviewManager { } } } + + private async spawnProcess(cmd: string, reg: RegExp, previewEngine: string, fileName: string, viewer: vscode.ViewColumn, fileUri: vscode.Uri, resourceViewColumn: vscode.ViewColumn): Promise { + return await new Promise((resolve, reject) => { + let childProcess: cp.ChildProcessWithoutNullStreams; + try { + childProcess = cp.spawn(cmd, null, { shell: true }); + } catch (e: unknown) { + console.warn(`[VSC-R] error: ${e as string}`); + reject(childProcess); + } + + this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process started`); + + // write the terminal output to R Markdown output stream + // (mostly just knitting information) + childProcess.stdout.on('data', (data: Buffer) => { + this.rMarkdownOutput.appendLine(data.toString('utf8')); + }); + + childProcess.stderr.on('error', (e: Error) => { + this.rMarkdownOutput.appendLine(`[VSC-R] knitting error: ${e.message}`); + reject(childProcess); + }); + + childProcess.stderr.on('data', + (data: Buffer) => { + const dat = data.toString('utf8'); + this.rMarkdownOutput.appendLine(dat); + const match = reg.exec(dat)?.[0]; + const previewUrl = this.constructUrl(previewEngine, match, fileName); + if (match) { + void this.showPreview(previewUrl, fileName, childProcess, viewer, fileUri, resourceViewColumn); + resolve(childProcess); + } else if (dat.includes('Execution halted')) { + reject(childProcess); + } + } + ); + + childProcess.on('exit', (code, signal) => { + this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process exited ` + + (signal ? `from signal '${signal}'` : `with exit code ${code}`)); + }); + }); + } } From 36a04661f9549ac47359d44e0570b4762b0404a9 Mon Sep 17 00:00:00 2001 From: ElianHugh Date: Mon, 5 Jul 2021 14:13:16 +1000 Subject: [PATCH 08/21] Bug squashing - Kill processes that error *after* knitting - Fix refresh bug that caused panel to go blank --- src/rmarkdown/preview.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 7b0ea2ca9..604ba80bd 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -46,6 +46,7 @@ class RMarkdownChildStore extends vscode.Disposable { this.store.add(child); } + // dispose child and remove from it from set public delete(child: RMarkdownChild): void { child.dispose(); this.store.delete(child); @@ -102,7 +103,11 @@ export class RMarkdownPreviewManager { await this.spawnProcess(cmd, reg, previewEngine, fileName, viewer, fileUri, currentViewColumn) .catch((cp: cp.ChildProcessWithoutNullStreams) => { void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); - cp.kill('SIGKILL'); + if (this.ChildStore.has(uri)) { + this.ChildStore.delete(this.ChildStore.get(uri)); + } else { + cp.kill('SIGKILL'); + } } ); }, @@ -115,7 +120,7 @@ export class RMarkdownPreviewManager { public refreshPanel(): void { if (this.activePreview) { this.activePreview.panel.webview.html = ''; - this.activePreview.panel.webview.html = getBrowserHtml(this.activePreview.uri); + this.activePreview.panel.webview.html = getBrowserHtml(this.activePreview.externalUri); } } @@ -243,25 +248,25 @@ export class RMarkdownPreviewManager { reject(childProcess); }); + childProcess.on('exit', (code, signal) => { + this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process exited ` + + (signal ? `from signal '${signal}'` : `with exit code ${code}`)); + }); + childProcess.stderr.on('data', (data: Buffer) => { const dat = data.toString('utf8'); this.rMarkdownOutput.appendLine(dat); const match = reg.exec(dat)?.[0]; const previewUrl = this.constructUrl(previewEngine, match, fileName); - if (match) { + if (dat.includes('Execution halted')) { + reject(childProcess); + } else if (match) { void this.showPreview(previewUrl, fileName, childProcess, viewer, fileUri, resourceViewColumn); resolve(childProcess); - } else if (dat.includes('Execution halted')) { - reject(childProcess); } } ); - - childProcess.on('exit', (code, signal) => { - this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process exited ` + - (signal ? `from signal '${signal}'` : `with exit code ${code}`)); - }); }); } } From 3e1d2088b549d1ed78bac15f7e1cbe22097684cd Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 5 Jul 2021 17:34:20 +1000 Subject: [PATCH 09/21] Cancellable knitting, spam prevention - Knitting can now be cancelled, and the process is properly killed - Can no longer spam the preview button of the same document while knitting, preventing duplicate webviews --- src/rmarkdown/preview.ts | 43 ++++++++++++++++++++++++++++++---------- src/util.ts | 8 ++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 604ba80bd..ef701acd1 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -80,6 +80,9 @@ export class RMarkdownPreviewManager { private ChildStore: RMarkdownChildStore = new RMarkdownChildStore; private activePreview: RMarkdownChild; private rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); + // uri that are in the process of knitting + // so that we can't spam the button + private busyUri: vscode.Uri[] = []; public async init(): Promise { this.rPath = await getRpath(false); @@ -96,24 +99,38 @@ export class RMarkdownPreviewManager { ); const reg: RegExp = this.constructRegex(previewEngine); - if (this.ChildStore.has(fileUri)) { + if (this.busyUri.includes(fileUri)) { + return; + } else if (this.ChildStore.has(fileUri)) { this.ChildStore.get(fileUri).panel.reveal(); } else { - await doWithProgress(async () => { - await this.spawnProcess(cmd, reg, previewEngine, fileName, viewer, fileUri, currentViewColumn) - .catch((cp: cp.ChildProcessWithoutNullStreams) => { - void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); + this.busyUri.push(fileUri); + await doWithProgress(async (token: vscode.CancellationToken) => { + await this.spawnProcess(cmd, reg, previewEngine, fileName, viewer, fileUri, currentViewColumn, token) + .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.'); + } if (this.ChildStore.has(uri)) { this.ChildStore.delete(this.ChildStore.get(uri)); } else { - cp.kill('SIGKILL'); + rejection.cp.kill('SIGKILL'); } } ); }, vscode.ProgressLocation.Notification, - `Knitting ${fileName}...` + `Knitting ${fileName}...`, + true ); + this.busyUri.forEach((item, index) => { + if (item === fileUri) { + this.busyUri.splice(index, 1); + } + }); } } @@ -225,14 +242,14 @@ export class RMarkdownPreviewManager { } } - private async spawnProcess(cmd: string, reg: RegExp, previewEngine: string, fileName: string, viewer: vscode.ViewColumn, fileUri: vscode.Uri, resourceViewColumn: vscode.ViewColumn): Promise { + private async spawnProcess(cmd: string, reg: RegExp, previewEngine: string, fileName: string, viewer: vscode.ViewColumn, fileUri: vscode.Uri, resourceViewColumn: vscode.ViewColumn, token: vscode.CancellationToken): Promise { return await new Promise((resolve, reject) => { let childProcess: cp.ChildProcessWithoutNullStreams; try { childProcess = cp.spawn(cmd, null, { shell: true }); } catch (e: unknown) { console.warn(`[VSC-R] error: ${e as string}`); - reject(childProcess); + reject({ cp: childProcess, wasCancelled: false }); } this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process started`); @@ -245,7 +262,7 @@ export class RMarkdownPreviewManager { childProcess.stderr.on('error', (e: Error) => { this.rMarkdownOutput.appendLine(`[VSC-R] knitting error: ${e.message}`); - reject(childProcess); + reject({ cp: childProcess, wasCancelled: false }); }); childProcess.on('exit', (code, signal) => { @@ -260,13 +277,17 @@ export class RMarkdownPreviewManager { const match = reg.exec(dat)?.[0]; const previewUrl = this.constructUrl(previewEngine, match, fileName); if (dat.includes('Execution halted')) { - reject(childProcess); + reject({ cp: childProcess, wasCancelled: false }); } else if (match) { void this.showPreview(previewUrl, fileName, childProcess, viewer, fileUri, resourceViewColumn); resolve(childProcess); } } ); + + token.onCancellationRequested(() => { + reject({cp: childProcess, wasCancelled: true}); + }); }); } } diff --git a/src/util.ts b/src/util.ts index 693fba19d..b9cac6a22 100644 --- a/src/util.ts +++ b/src/util.ts @@ -230,17 +230,17 @@ 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: () => T | Promise, location: string | vscode.ProgressLocation = 'rHelpPages', title?: string): Promise { +export async function doWithProgress(cb: (token?: vscode.CancellationToken) => 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, - cancellable: false, + cancellable: cancellable ?? false, title: title }; let ret: T; - await vscode.window.withProgress(options, async () => { + await vscode.window.withProgress(options, async (_progress, token) => { const retPromise = new Promise((resolve) => setTimeout(() => { - const ret = cb(); + const ret = cb(token); resolve(ret); })); ret = await retPromise; From 634384fa49d80018b633a36caeae9fca64735449 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Mon, 5 Jul 2021 19:58:16 +1000 Subject: [PATCH 10/21] Show output on error, misc changes - busyUri probably makes more sense as a set, rather than an array - Show rmarkdown output on error - change return values for RMarkdownChildStore methods to what is expected of a set --- src/rmarkdown/preview.ts | 65 ++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index ef701acd1..ebfb07eb6 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -42,17 +42,17 @@ class RMarkdownChildStore extends vscode.Disposable { }); } - public add(child: RMarkdownChild): void { - this.store.add(child); + public add(child: RMarkdownChild): Set { + return this.store.add(child); } - // dispose child and remove from it from set - public delete(child: RMarkdownChild): void { + // dispose child and remove it from set + public delete(child: RMarkdownChild): boolean { child.dispose(); - this.store.delete(child); + return this.store.delete(child); } - public get(uri: vscode.Uri) { + public get(uri: vscode.Uri): RMarkdownChild { for (const child of this.store) { if (child.uri === uri) { return child; @@ -61,7 +61,7 @@ class RMarkdownChildStore extends vscode.Disposable { return undefined; } - public has(uri: vscode.Uri) { + public has(uri: vscode.Uri): boolean { for (const child of this.store) { if (child.uri === uri) { return true; @@ -77,16 +77,19 @@ class RMarkdownChildStore extends vscode.Disposable { export class RMarkdownPreviewManager { private rPath: string; - private ChildStore: RMarkdownChildStore = new RMarkdownChildStore; - private activePreview: RMarkdownChild; private rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); + + // the currently selected RMarkdown preview + private activePreview: RMarkdownChild; + // store of all open RMarkdown previews + private childStore: RMarkdownChildStore = new RMarkdownChildStore; // uri that are in the process of knitting - // so that we can't spam the button - private busyUri: vscode.Uri[] = []; + // so that we can't spam the preview button + private busyUriStore: Set = new Set(); public async init(): Promise { this.rPath = await getRpath(false); - extensionContext.subscriptions.push(this.ChildStore); + extensionContext.subscriptions.push(this.childStore); } public async previewRmd(viewer: vscode.ViewColumn, uri?: vscode.Uri): Promise { @@ -99,12 +102,12 @@ export class RMarkdownPreviewManager { ); const reg: RegExp = this.constructRegex(previewEngine); - if (this.busyUri.includes(fileUri)) { + if (this.busyUriStore.has(fileUri)) { return; - } else if (this.ChildStore.has(fileUri)) { - this.ChildStore.get(fileUri).panel.reveal(); + } else if (this.childStore.has(fileUri)) { + this.childStore.get(fileUri).panel.reveal(); } else { - this.busyUri.push(fileUri); + this.busyUriStore.add(fileUri); await doWithProgress(async (token: vscode.CancellationToken) => { await this.spawnProcess(cmd, reg, previewEngine, fileName, viewer, fileUri, currentViewColumn, token) .catch((rejection: { @@ -113,9 +116,12 @@ export class RMarkdownPreviewManager { }) => { 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); } - if (this.ChildStore.has(uri)) { - this.ChildStore.delete(this.ChildStore.get(uri)); + // this can occur when a successfuly knitted document is later altered (while still being previewed) + // and subsequently fails to knit + if (this.childStore.has(uri)) { + this.childStore.delete(this.childStore.get(uri)); } else { rejection.cp.kill('SIGKILL'); } @@ -126,11 +132,7 @@ export class RMarkdownPreviewManager { `Knitting ${fileName}...`, true ); - this.busyUri.forEach((item, index) => { - if (item === fileUri) { - this.busyUri.splice(index, 1); - } - }); + this.busyUriStore.delete(fileUri); } } @@ -141,9 +143,16 @@ export class RMarkdownPreviewManager { } } + // show the source uri for the current preview. + // has a few idiosyncracies with view columns due to some limitations with + // vscode api. the view column will be set in order of priority: + // 1. the original document's view column when the preview button was pressed + // 2. the current webview's view column + // 3. the current active editor + // this is because we cannot tell the view column of a file if it is not visible + // (e.g., is an unopened tab) public async showSource(): Promise { if (this.activePreview) { - // to fix, kind of buggy await vscode.commands.executeCommand('vscode.open', this.activePreview.uri, { preserveFocus: false, preview: false, @@ -182,7 +191,7 @@ export class RMarkdownPreviewManager { // (primarily used in killing the child process, but also // general state tracking) const childProcess = new RMarkdownChild(title, cp, panel, resourceViewColumn, fileUri, externalUri); - this.ChildStore.add(childProcess); + this.childStore.add(childProcess); if (isHost()) { await shareBrowser(url, title); @@ -193,7 +202,7 @@ export class RMarkdownPreviewManager { // clear values this.activePreview === childProcess ? undefined : this.activePreview; void setContext('r.preview.active', false); - this.ChildStore.delete(childProcess); + this.childStore.delete(childProcess); if (isHost()) { closeBrowser(url); @@ -223,6 +232,9 @@ export class RMarkdownPreviewManager { } } + // construct a regex that is used in getting the path to the + // *served* rmd file. this can differ depending on the + // engine used, and can be expanded in the future. private constructRegex(previewEngine: string): RegExp { switch (previewEngine) { // the rmarkdown::run url is of the structure: @@ -242,6 +254,7 @@ export class RMarkdownPreviewManager { } } + // spawn a nodejs child process for knitting, and return the output's url for webview purposes private async spawnProcess(cmd: string, reg: RegExp, previewEngine: string, fileName: string, viewer: vscode.ViewColumn, fileUri: vscode.Uri, resourceViewColumn: vscode.ViewColumn, token: vscode.CancellationToken): Promise { return await new Promise((resolve, reject) => { let childProcess: cp.ChildProcessWithoutNullStreams; From 1f361f66c1629527687964b21dab1c4bf9021e3c Mon Sep 17 00:00:00 2001 From: ElianHugh Date: Tue, 6 Jul 2021 11:44:52 +1000 Subject: [PATCH 11/21] Messy implementation of file watcher Changes preview engine to rmarkdown::render, and relies on fs.watch to check for updates. This is a pretty messy implementation, and will be cleaned up shortly --- src/rmarkdown/preview.ts | 304 +++++++++++++++++++++------------------ 1 file changed, 163 insertions(+), 141 deletions(-) diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index ebfb07eb6..2ac8b8c7d 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -1,11 +1,12 @@ import * as cp from 'child_process'; import * as vscode from 'vscode'; +import * as fs from 'fs-extra'; +import * as os from 'os'; -import { getBrowserHtml } from '../session'; -import { closeBrowser, isHost, shareBrowser } from '../liveshare'; -import { config, doWithProgress, getRpath, setContext } from '../util'; +import { doWithProgress, getRpath, readContent, setContext } from '../util'; import { extensionContext } from '../extension'; +import path = require('path'); class RMarkdownChild extends vscode.Disposable { title: string; @@ -13,12 +14,16 @@ class RMarkdownChild extends vscode.Disposable { panel: vscode.WebviewPanel; resourceViewColumn: vscode.ViewColumn; uri: vscode.Uri; - externalUri: vscode.Uri; + outputUri: vscode.Uri; + dir: string; + fileWatcher: fs.FSWatcher; - constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, uri: vscode.Uri, externalUri: vscode.Uri) { + constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, uri: vscode.Uri, dir: string) { super(() => { - this.cp.kill('SIGKILL'); + this.cp?.kill('SIGKILL'); this.panel?.dispose(); + this.fileWatcher?.close(); + fs.removeSync(this.outputUri.path); }); this.title = title; @@ -26,7 +31,19 @@ class RMarkdownChild extends vscode.Disposable { this.panel = panel; this.resourceViewColumn = resourceViewColumn; this.uri = uri; - this.externalUri = externalUri; + this.outputUri = outputUri; + this.dir = dir; + } + + startFileWatcher(RMarkdownPreviewManager: RMarkdownPreviewManager) { + let fsTimeout: NodeJS.Timeout; + const docWatcher = fs.watch(this.uri.path, {}, () => { + if (!fsTimeout) { + fsTimeout = setTimeout(() => { fsTimeout = null; }, 1000); + void RMarkdownPreviewManager.updatePreview(this); + } + }); + this.fileWatcher = docWatcher; } } @@ -78,6 +95,7 @@ class RMarkdownChildStore extends vscode.Disposable { export class RMarkdownPreviewManager { private rPath: string; private rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); + private watcherDir = path.join(os.homedir(), '.vscode-R'); // the currently selected RMarkdown preview private activePreview: RMarkdownChild; @@ -95,51 +113,24 @@ export class RMarkdownPreviewManager { public async previewRmd(viewer: vscode.ViewColumn, uri?: vscode.Uri): Promise { const fileUri = uri ?? vscode.window.activeTextEditor.document.uri; const fileName = fileUri.path.substring(fileUri.path.lastIndexOf('/') + 1); - const previewEngine: string = config().get('rmarkdown.previewEngine'); const currentViewColumn: vscode.ViewColumn = vscode.window.activeTextEditor.viewColumn ?? vscode.ViewColumn.Active; - const cmd = ( - `${this.rPath} --silent --slave --no-save --no-restore -e "${previewEngine}('${fileUri.path}')"` - ); - const reg: RegExp = this.constructRegex(previewEngine); if (this.busyUriStore.has(fileUri)) { return; } else if (this.childStore.has(fileUri)) { this.childStore.get(fileUri).panel.reveal(); } else { - this.busyUriStore.add(fileUri); - await doWithProgress(async (token: vscode.CancellationToken) => { - await this.spawnProcess(cmd, reg, previewEngine, fileName, viewer, fileUri, currentViewColumn, token) - .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); - } - // this can occur when a successfuly knitted document is later altered (while still being previewed) - // and subsequently fails to knit - if (this.childStore.has(uri)) { - this.childStore.delete(this.childStore.get(uri)); - } else { - rejection.cp.kill('SIGKILL'); - } - } - ); - }, - vscode.ProgressLocation.Notification, - `Knitting ${fileName}...`, - true - ); - this.busyUriStore.delete(fileUri); + await this.retrieveSpawnData(fileUri, fileName, viewer, currentViewColumn, uri); } } - public refreshPanel(): void { - if (this.activePreview) { + + public async refreshPanel(child?: RMarkdownChild): Promise { + if (child) { + child.panel.webview.html = await this.loadHtml(child); + } else if (this.activePreview) { this.activePreview.panel.webview.html = ''; - this.activePreview.panel.webview.html = getBrowserHtml(this.activePreview.externalUri); + this.activePreview.panel.webview.html = await this.loadHtml(this.activePreview); } } @@ -163,15 +154,118 @@ export class RMarkdownPreviewManager { public openExternalBrowser(): void { if (this.activePreview) { - void vscode.env.openExternal(this.activePreview.externalUri); + void vscode.env.openExternal(this.activePreview.outputUri); } } - private async showPreview(url: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, fileUri: vscode.Uri, resourceViewColumn: vscode.ViewColumn): Promise { - // construct webview and its related html - console.info(`[showPreview] uri: ${url}`); - const uri = vscode.Uri.parse(url); - const externalUri = await vscode.env.asExternalUri(uri); + private async retrieveSpawnData(fileUri: vscode.Uri, fileName: string, viewer: vscode.ViewColumn, currentViewColumn: vscode.ViewColumn, uri?: vscode.Uri) { + this.busyUriStore.add(fileUri); + await doWithProgress(async (token: vscode.CancellationToken) => { + await this.spawnProcess(fileUri, 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); + } + // this can occur when a successfuly knitted document is later altered (while still being previewed) + // and subsequently fails to knit + if (this.childStore.has(uri)) { + this.childStore.delete(this.childStore.get(uri)); + } else { + rejection.cp.kill('SIGKILL'); + } + }); + this.busyUriStore.delete(fileUri); + } + + private async spawnProcess(fileUri: vscode.Uri, 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 cmd = ( + `${this.rPath} --silent --slave --no-save --no-restore -e ` + + `"cat('${lim}', rmarkdown::render('${String(fileUri.path)}', output_dir = '${this.watcherDir}'), '${lim}', sep=''); while(TRUE) Sys.sleep(1)" ` + ); + + 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 }); + } + + this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process started`); + + childProcess.stdout.on('data', + (data: Buffer) => { + const dat = data.toString('utf8'); + this.rMarkdownOutput.appendLine(dat); + const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); + if (outputUrl) { + if (viewer !== undefined) { + void this.openPreview( + vscode.Uri.parse(outputUrl), + fileUri, + fileName, + childProcess, + viewer, + currentViewColumn + ); + } + resolve(childProcess); + } + } + ); + + childProcess.stderr.on('data', (data: Buffer) => { + const dat = data.toString('utf8'); + this.rMarkdownOutput.appendLine(dat); + if (dat.includes('Execution halted')) { + reject({ cp: childProcess, wasCancelled: false }); + } + }); + + childProcess.on('exit', (code, signal) => { + this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process exited ` + + (signal ? `from signal '${signal}'` : `with exit code ${code}`)); + }); + + token?.onCancellationRequested(() => { + reject({ cp: childProcess, wasCancelled: true }); + }); + }); + } + + public async updatePreview(child: RMarkdownChild) { + child.cp.kill('SIGKILL'); + + const spawn: cp.ChildProcessWithoutNullStreams | void = await this.spawnProcess(child.uri, child.title).catch((rejection: { + cp: cp.ChildProcessWithoutNullStreams, + wasCancelled?: boolean + }) => { + void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); + this.rMarkdownOutput.show(true); + rejection.cp.kill('SIGINT'); + this.childStore.get(child.uri).dispose(); + }); + + if (spawn) { + child.cp = spawn; + } + + await this.refreshPanel(child); + } + + private async openPreview(outputUri: vscode.Uri, fileUri: vscode.Uri, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn): Promise { + const dir = path.dirname(outputUri.path); const panel = vscode.window.createWebviewPanel( 'previewRmd', `Preview ${title}`, @@ -183,19 +277,28 @@ export class RMarkdownPreviewManager { enableFindWidget: true, enableScripts: true, retainContextWhenHidden: true, + localResourceRoots: [vscode.Uri.file(dir)], }); - panel.webview.html = getBrowserHtml(externalUri); + + // 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 // general state tracking) - const childProcess = new RMarkdownChild(title, cp, panel, resourceViewColumn, fileUri, externalUri); + const childProcess = new RMarkdownChild( + title, + cp, + panel, + resourceViewColumn, + outputUri, + fileUri, + dir + ); this.childStore.add(childProcess); - - if (isHost()) { - await shareBrowser(url, title); - } + const html = await this.loadHtml(childProcess); + panel.webview.html = html; + childProcess.startFileWatcher(this); // state change panel.onDidDispose(() => { @@ -203,10 +306,6 @@ export class RMarkdownPreviewManager { this.activePreview === childProcess ? undefined : this.activePreview; void setContext('r.preview.active', false); this.childStore.delete(childProcess); - - if (isHost()) { - closeBrowser(url); - } }); panel.onDidChangeViewState(({ webviewPanel }) => { @@ -217,90 +316,13 @@ export class RMarkdownPreviewManager { }); } - private constructUrl(previewEngine: string, match: string, fileName?: string): string { - switch (previewEngine) { - case 'rmarkdown::run': { - return `http://${match}/${fileName}`; - } - case 'xaringan::infinite_moon_reader': { - return `http://${match}.html`; - } - default: { - console.error(`[PreviewProvider] unsupported preview engine supplied as argument: ${previewEngine}`); - break; - } - } - } - - // construct a regex that is used in getting the path to the - // *served* rmd file. this can differ depending on the - // engine used, and can be expanded in the future. - private constructRegex(previewEngine: string): RegExp { - switch (previewEngine) { - // the rmarkdown::run url is of the structure: - // http://127.0.0.1:port/file.Rmd - case 'rmarkdown::run': { - return /(?<=http:\/\/)[0-9.:]*/g; - } - // the inf_mr output url is of the structure: - // http://127.0.0.1:port/path/to/file.html - case 'xaringan::infinite_moon_reader': { - return /(?<=http:\/\/)(.*)(?=\.html)/g; - } - default: { - console.error(`[PreviewProvider] unsupported preview engine supplied as argument: ${previewEngine}`); - break; - } - } + private async loadHtml(childProcess: RMarkdownChild): Promise { + const content = await readContent(childProcess.outputUri.path, 'utf8'); + const html = content.replace('', '') + .replace(/<(\w+)\s+(href|src)="(?!\w+:)/g, + `<$1 $2="${String(childProcess.panel.webview.asWebviewUri(vscode.Uri.file(childProcess.dir)))}/`); + return html; } - // spawn a nodejs child process for knitting, and return the output's url for webview purposes - private async spawnProcess(cmd: string, reg: RegExp, previewEngine: string, fileName: string, viewer: vscode.ViewColumn, fileUri: vscode.Uri, resourceViewColumn: vscode.ViewColumn, token: vscode.CancellationToken): Promise { - return await new Promise((resolve, reject) => { - let childProcess: cp.ChildProcessWithoutNullStreams; - try { - childProcess = cp.spawn(cmd, null, { shell: true }); - } catch (e: unknown) { - console.warn(`[VSC-R] error: ${e as string}`); - reject({ cp: childProcess, wasCancelled: false }); - } - - this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process started`); - - // write the terminal output to R Markdown output stream - // (mostly just knitting information) - childProcess.stdout.on('data', (data: Buffer) => { - this.rMarkdownOutput.appendLine(data.toString('utf8')); - }); - - childProcess.stderr.on('error', (e: Error) => { - this.rMarkdownOutput.appendLine(`[VSC-R] knitting error: ${e.message}`); - reject({ cp: childProcess, wasCancelled: false }); - }); - - childProcess.on('exit', (code, signal) => { - this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process exited ` + - (signal ? `from signal '${signal}'` : `with exit code ${code}`)); - }); - - childProcess.stderr.on('data', - (data: Buffer) => { - const dat = data.toString('utf8'); - this.rMarkdownOutput.appendLine(dat); - const match = reg.exec(dat)?.[0]; - const previewUrl = this.constructUrl(previewEngine, match, fileName); - if (dat.includes('Execution halted')) { - reject({ cp: childProcess, wasCancelled: false }); - } else if (match) { - void this.showPreview(previewUrl, fileName, childProcess, viewer, fileUri, resourceViewColumn); - resolve(childProcess); - } - } - ); - - token.onCancellationRequested(() => { - reject({cp: childProcess, wasCancelled: true}); - }); - }); - } } + From e2093715aad55d2e2f793615f93280f771a7a5c4 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Tue, 6 Jul 2021 13:49:33 +0800 Subject: [PATCH 12/21] Make extDir global --- package.json | 1 + src/extension.ts | 13 +++++++++++++ src/rTerminal.ts | 6 +++--- src/rmarkdown/preview.ts | 8 ++++---- src/session.ts | 15 ++++----------- yarn.lock | 5 +++++ 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 1b1e23b3e..69acbd995 100644 --- a/package.json +++ b/package.json @@ -1388,6 +1388,7 @@ "dependencies": { "bootstrap": "^5.0.1", "cheerio": "1.0.0-rc.10", + "crypto": "^1.0.1", "datatables.net": "^1.10.25", "datatables.net-bs4": "^1.10.25", "datatables.net-fixedheader-jqui": "^3.1.9", diff --git a/src/extension.ts b/src/extension.ts index 4527d2fb0..65ca6843a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,9 @@ // interfaces, functions, etc. provided by vscode import * as vscode from 'vscode'; +import * as os from 'os'; +import path = require('path'); +import fs = require('fs'); // functions etc. implemented in this extension import * as preview from './preview'; @@ -22,6 +25,8 @@ import * as httpgdViewer from './plotViewer'; import { RMarkdownPreviewManager } from './rmarkdown/preview'; // global objects used in other files +export const extDir: string = path.join(os.homedir(), '.vscode-R'); +export const tmpDir: string = path.join(extDir, 'tmp'); export let rWorkspace: workspaceViewer.WorkspaceDataProvider | undefined = undefined; export let globalRHelp: rHelp.RHelp | undefined = undefined; export let extensionContext: vscode.ExtensionContext; @@ -31,6 +36,14 @@ export let rMarkdownPreview: RMarkdownPreviewManager | undefined = undefined; // Called (once) when the extension is activated export async function activate(context: vscode.ExtensionContext): Promise { + if (!fs.existsSync(extDir)) { + fs.mkdirSync(extDir); + } + + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir); + } + // 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 diff --git a/src/rTerminal.ts b/src/rTerminal.ts index 8262f7872..9a864fd29 100644 --- a/src/rTerminal.ts +++ b/src/rTerminal.ts @@ -6,11 +6,11 @@ import { isDeepStrictEqual } from 'util'; import * as vscode from 'vscode'; -import { extensionContext } from './extension'; +import { extensionContext, extDir } from './extension'; import * as util from './util'; import * as selection from './selection'; import { getSelection } from './selection'; -import { removeSessionFiles, watcherDir } from './session'; +import { removeSessionFiles } from './session'; import { config, delay, getRterm } from './util'; import { rGuestService, isGuestSession } from './liveshare'; export let rTerm: vscode.Terminal; @@ -124,7 +124,7 @@ export async function createRTerm(preserveshow?: boolean): Promise { R_PROFILE_USER_OLD: process.env.R_PROFILE_USER, R_PROFILE_USER: newRprofile, VSCODE_INIT_R: initR, - VSCODE_WATCHER_DIR: watcherDir + VSCODE_WATCHER_DIR: extDir }; } rTerm = vscode.window.createTerminal(termOptions); diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 2ac8b8c7d..3ce586c65 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -2,11 +2,11 @@ import * as cp from 'child_process'; import * as vscode from 'vscode'; import * as fs from 'fs-extra'; -import * as os from 'os'; import { doWithProgress, getRpath, readContent, setContext } from '../util'; -import { extensionContext } from '../extension'; +import { extensionContext, tmpDir } from '../extension'; import path = require('path'); +import crypto = require('crypto'); class RMarkdownChild extends vscode.Disposable { title: string; @@ -95,7 +95,6 @@ class RMarkdownChildStore extends vscode.Disposable { export class RMarkdownPreviewManager { private rPath: string; private rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); - private watcherDir = path.join(os.homedir(), '.vscode-R'); // the currently selected RMarkdown preview private activePreview: RMarkdownChild; @@ -189,9 +188,10 @@ export class RMarkdownPreviewManager { 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(fileUri.fsPath).digest('hex') + '.html'); const cmd = ( `${this.rPath} --silent --slave --no-save --no-restore -e ` + - `"cat('${lim}', rmarkdown::render('${String(fileUri.path)}', output_dir = '${this.watcherDir}'), '${lim}', sep=''); while(TRUE) Sys.sleep(1)" ` + `"cat('${lim}', rmarkdown::render('${String(fileUri.path)}', output_file = '${outputFile}'), '${lim}', sep='')"` ); let childProcess: cp.ChildProcessWithoutNullStreams; diff --git a/src/session.ts b/src/session.ts index 1102c1e60..3036a4be5 100644 --- a/src/session.ts +++ b/src/session.ts @@ -14,12 +14,11 @@ import { FSWatcher } from 'fs-extra'; import { config, readContent } from './util'; import { purgeAddinPickerItems, dispatchRStudioAPICall } from './rstudioapi'; -import { rWorkspace, globalRHelp, globalHttpgdManager } from './extension'; +import { extDir, rWorkspace, globalRHelp, globalHttpgdManager } from './extension'; import { UUID, rHostService, rGuestService, isLiveShare, isHost, isGuestSession, closeBrowser, guestResDir, shareBrowser, openVirtualDoc, shareWorkspace } from './liveshare'; export let globalenv: any; let resDir: string; -export let watcherDir: string; export let requestFile: string; export let requestLockFile: string; let requestTimeStamp: number; @@ -44,22 +43,16 @@ let activeBrowserExternalUri: Uri; export function deploySessionWatcher(extensionPath: string): void { console.info(`[deploySessionWatcher] extensionPath: ${extensionPath}`); resDir = path.join(extensionPath, 'dist', 'resources'); - watcherDir = path.join(os.homedir(), '.vscode-R'); - console.info(`[deploySessionWatcher] watcherDir: ${watcherDir}`); - if (!fs.existsSync(watcherDir)) { - console.info('[deploySessionWatcher] watcherDir not exists, create directory'); - fs.mkdirSync(watcherDir); - } const initPath = path.join(extensionPath, 'R', 'init.R'); - const linkPath = path.join(watcherDir, 'init.R'); + const linkPath = path.join(extDir, 'init.R'); fs.writeFileSync(linkPath, `local(source("${initPath.replace(/\\/g, '\\\\')}", chdir = TRUE, local = TRUE))\n`); } export function startRequestWatcher(sessionStatusBarItem: StatusBarItem): void { console.info('[startRequestWatcher] Starting'); - requestFile = path.join(watcherDir, 'request.log'); - requestLockFile = path.join(watcherDir, 'request.lock'); + requestFile = path.join(extDir, 'request.log'); + requestLockFile = path.join(extDir, 'request.lock'); requestTimeStamp = 0; responseTimeStamp = 0; if (!fs.existsSync(requestLockFile)) { diff --git a/yarn.lock b/yarn.lock index 68254321f..32330d6c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -913,6 +913,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + css-select@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067" From dc3a9b5e462c634b3583b51a005380fd5b5ec7bd Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:28:56 +1000 Subject: [PATCH 13/21] Set -> Map, cache html content, toggle style, ongoing refactor - CSS can now be toggled between vscode theme and base doc theme - Changed store from set to map - now cache html in RMarkdownChild, so that toggling doesn't require a re-read of the source html - Ensure output is html - Refactor is ongoing --- package.json | 24 +++--- src/extension.ts | 3 +- src/rmarkdown/preview.ts | 161 +++++++++++++++++++++++---------------- src/session.ts | 2 +- 4 files changed, 109 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index 69acbd995..d190e097b 100644 --- a/package.json +++ b/package.json @@ -556,6 +556,12 @@ "icon": "$(go-to-file)", "category": "R" }, + { + "title": "Toggle Style", + "category": "R", + "command": "r.markdown.preview.toggleStyle", + "icon": "$(symbol-color)" + }, { "title": "Launch RStudio Addin", "category": "R", @@ -864,6 +870,11 @@ "when": "resourceScheme =~ /webview/ && r.preview.active", "group": "navigation@2" }, + { + "command": "r.markdown.preview.toggleStyle", + "when": "resourceScheme =~ /webview/ && r.preview.active", + "group": "navigation@3" + }, { "command": "r.markdown.preview.refresh", "when": "resourceScheme =~ /webview/ && r.preview.active", @@ -1178,19 +1189,6 @@ "default": "rgba(128, 128, 128, 0.1)", "description": "RMarkdown chunk background color in RGBA or RGB value. Defaults to rgba(128, 128, 128, 0.1). Leave it empty to disable it (use default editor background color). Reload VS Code after changing settings.\n\nLearn how to set colors: https://www.w3schools.com/css/css_colors_rgb.asp.\n\nExamples for syntax rgba(, , , ):\nrgba(128, 128, 128, 0.1)\nrgba(128, 128, 128, 0.3)\nrgba(255, 165, 0, 0.1)\n\n" }, - "r.rmarkdown.previewEngine": { - "type": "string", - "enum": [ - "rmarkdown::run", - "xaringan::infinite_moon_reader" - ], - "enumDescriptions": [ - "Use rmarkdown::run() to preview documents", - "Use xaringan::infinite_moon_reader() to preview documents" - ], - "description": "What package command to use for previewing R Markdown documents.", - "default": "rmarkdown::run" - }, "r.helpPanel.enableSyntaxHighlighting": { "type": "boolean", "default": true, diff --git a/src/extension.ts b/src/extension.ts index 65ca6843a..c0223c82a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -96,8 +96,9 @@ export async function activate(context: vscode.ExtensionContext): Promise rMarkdownPreview.previewRmd(vscode.ViewColumn.Beside), 'r.markdown.showPreview': (uri: vscode.Uri) => rMarkdownPreview.previewRmd(vscode.ViewColumn.Active, uri), 'r.markdown.preview.refresh': () => rMarkdownPreview.refreshPanel(), - 'r.markdown.preview.openExternal': () => rMarkdownPreview.openExternalBrowser(), + 'r.markdown.preview.openExternal': async () => await rMarkdownPreview.openExternalBrowser(), 'r.markdown.preview.showSource': () => rMarkdownPreview.showSource(), + 'r.markdown.preview.toggleStyle': () => rMarkdownPreview.toggleTheme(), // editor independent commands 'r.createGitignore': rGitignore.createGitignore, diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 3ce586c65..6ff527b1b 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -3,7 +3,7 @@ import * as cp from 'child_process'; import * as vscode from 'vscode'; import * as fs from 'fs-extra'; -import { doWithProgress, getRpath, readContent, setContext } from '../util'; +import { config, doWithProgress, getRpath, readContent, setContext } from '../util'; import { extensionContext, tmpDir } from '../extension'; import path = require('path'); import crypto = require('crypto'); @@ -13,12 +13,13 @@ class RMarkdownChild extends vscode.Disposable { cp: cp.ChildProcessWithoutNullStreams; panel: vscode.WebviewPanel; resourceViewColumn: vscode.ViewColumn; - uri: vscode.Uri; + // todo, restructure, as uri is now used as a key + uri: vscode.Uri outputUri: vscode.Uri; - dir: string; + htmlContent: string; fileWatcher: fs.FSWatcher; - constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, uri: vscode.Uri, dir: string) { + constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, uri: vscode.Uri, htmlContent: string, RMarkdownPreviewManager: RMarkdownPreviewManager) { super(() => { this.cp?.kill('SIGKILL'); this.panel?.dispose(); @@ -32,62 +33,53 @@ class RMarkdownChild extends vscode.Disposable { this.resourceViewColumn = resourceViewColumn; this.uri = uri; this.outputUri = outputUri; - this.dir = dir; + this.htmlContent = htmlContent; + this.startFileWatcher(RMarkdownPreviewManager); } - startFileWatcher(RMarkdownPreviewManager: RMarkdownPreviewManager) { + private startFileWatcher(RMarkdownPreviewManager: RMarkdownPreviewManager) { let fsTimeout: NodeJS.Timeout; - const docWatcher = fs.watch(this.uri.path, {}, () => { + const fileWatcher = fs.watch(this.uri.path, {}, () => { if (!fsTimeout) { fsTimeout = setTimeout(() => { fsTimeout = null; }, 1000); void RMarkdownPreviewManager.updatePreview(this); } }); - this.fileWatcher = docWatcher; + this.fileWatcher = fileWatcher; } } class RMarkdownChildStore extends vscode.Disposable { - private store: Set = new Set(); + private store: Map = new Map(); constructor() { super((): void => { for (const child of this.store) { - child.dispose(); + child[1].dispose(); } this.store.clear(); }); } - public add(child: RMarkdownChild): Set { - return this.store.add(child); + public add(uri:vscode.Uri, child: RMarkdownChild): Map { + return this.store.set(uri, child); } // dispose child and remove it from set - public delete(child: RMarkdownChild): boolean { - child.dispose(); - return this.store.delete(child); + public delete(uri: vscode.Uri): boolean { + this.store.get(uri).dispose(); + return this.store.delete(uri); } public get(uri: vscode.Uri): RMarkdownChild { - for (const child of this.store) { - if (child.uri === uri) { - return child; - } - } - return undefined; + return this.store.get(uri); } public has(uri: vscode.Uri): boolean { - for (const child of this.store) { - if (child.uri === uri) { - return true; - } - } - return false; + return this.store.has(uri); } - [Symbol.iterator](): Iterator { + [Symbol.iterator]() { return this.store[Symbol.iterator](); } } @@ -104,6 +96,9 @@ export class RMarkdownPreviewManager { // so that we can't spam the preview button private busyUriStore: Set = new Set(); + // todo, better name? enum? + private vscodeTheme = true; + public async init(): Promise { this.rPath = await getRpath(false); extensionContext.subscriptions.push(this.childStore); @@ -117,19 +112,28 @@ export class RMarkdownPreviewManager { if (this.busyUriStore.has(fileUri)) { return; } else if (this.childStore.has(fileUri)) { - this.childStore.get(fileUri).panel.reveal(); + this.childStore.get(fileUri)?.panel.reveal(); } else { - await this.retrieveSpawnData(fileUri, fileName, viewer, currentViewColumn, uri); + this.busyUriStore.add(fileUri); + await this.knitWithProgress(fileUri, fileName, viewer, currentViewColumn, uri); + this.busyUriStore.delete(fileUri); } } - - public async refreshPanel(child?: RMarkdownChild): Promise { + // todo, should this trigger a re-knit? + public refreshPanel(child?: RMarkdownChild): void { if (child) { - child.panel.webview.html = await this.loadHtml(child); + child.panel.webview.html = this.getHtmlContent(child); } else if (this.activePreview) { this.activePreview.panel.webview.html = ''; - this.activePreview.panel.webview.html = await this.loadHtml(this.activePreview); + this.activePreview.panel.webview.html = this.getHtmlContent(this.activePreview); + } + } + + public toggleTheme(): void { + this.vscodeTheme = !this.vscodeTheme; + for (const child of this.childStore) { + this.refreshPanel(child[1]); } } @@ -153,14 +157,15 @@ export class RMarkdownPreviewManager { public openExternalBrowser(): void { if (this.activePreview) { - void vscode.env.openExternal(this.activePreview.outputUri); + void vscode.env.openExternal( + this.activePreview.outputUri + ); } } - private async retrieveSpawnData(fileUri: vscode.Uri, fileName: string, viewer: vscode.ViewColumn, currentViewColumn: vscode.ViewColumn, uri?: vscode.Uri) { - this.busyUriStore.add(fileUri); + private async knitWithProgress(fileUri: vscode.Uri, fileName: string, viewer: vscode.ViewColumn, currentViewColumn: vscode.ViewColumn, uri?: vscode.Uri) { await doWithProgress(async (token: vscode.CancellationToken) => { - await this.spawnProcess(fileUri, fileName, token, viewer, currentViewColumn); + await this.knitDocument(fileUri, fileName, token, viewer, currentViewColumn); }, vscode.ProgressLocation.Notification, `Knitting ${fileName}...`, @@ -169,6 +174,8 @@ export class RMarkdownPreviewManager { cp: cp.ChildProcessWithoutNullStreams, wasCancelled?: boolean }) => { + // todo, this section may need to be cleaned up a bit, + // move the non-rejection catch to the await knitDocument? 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); @@ -176,22 +183,23 @@ export class RMarkdownPreviewManager { // this can occur when a successfuly knitted document is later altered (while still being previewed) // and subsequently fails to knit if (this.childStore.has(uri)) { - this.childStore.delete(this.childStore.get(uri)); + this.childStore.delete(uri); } else { rejection.cp.kill('SIGKILL'); } }); - this.busyUriStore.delete(fileUri); } - private async spawnProcess(fileUri: vscode.Uri, fileName: string, token?: vscode.CancellationToken, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn) { + private async knitDocument(fileUri: vscode.Uri, 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(fileUri.fsPath).digest('hex') + '.html'); const cmd = ( `${this.rPath} --silent --slave --no-save --no-restore -e ` + - `"cat('${lim}', rmarkdown::render('${String(fileUri.path)}', output_file = '${outputFile}'), '${lim}', sep='')"` + `"cat('${lim}', + rmarkdown::render('${String(fileUri.path)}', output_format = rmarkdown::html_document(), output_file = '${outputFile}'), + '${lim}', sep='')"` ); let childProcess: cp.ChildProcessWithoutNullStreams; @@ -244,28 +252,27 @@ export class RMarkdownPreviewManager { }); } - public async updatePreview(child: RMarkdownChild) { - child.cp.kill('SIGKILL'); + public async updatePreview(child: RMarkdownChild): Promise { + child.cp?.kill('SIGKILL'); - const spawn: cp.ChildProcessWithoutNullStreams | void = await this.spawnProcess(child.uri, child.title).catch((rejection: { + const childProcess: cp.ChildProcessWithoutNullStreams | void = await this.knitDocument(child.uri, child.title).catch((rejection: { cp: cp.ChildProcessWithoutNullStreams, wasCancelled?: boolean }) => { void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); this.rMarkdownOutput.show(true); rejection.cp.kill('SIGINT'); - this.childStore.get(child.uri).dispose(); + this.childStore.get(child.uri)?.dispose(); }); - if (spawn) { - child.cp = spawn; + if (childProcess) { + child.cp = childProcess; } - await this.refreshPanel(child); + this.refreshPanel(child); } private async openPreview(outputUri: vscode.Uri, fileUri: vscode.Uri, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn): Promise { - const dir = path.dirname(outputUri.path); const panel = vscode.window.createWebviewPanel( 'previewRmd', `Preview ${title}`, @@ -277,10 +284,9 @@ export class RMarkdownPreviewManager { enableFindWidget: true, enableScripts: true, retainContextWhenHidden: true, - localResourceRoots: [vscode.Uri.file(dir)], + localResourceRoots: [vscode.Uri.file(tmpDir)], }); - - + const htmlContent = await readContent(outputUri.path, 'utf8'); // Push the new rmd webview to the open proccesses array, // to keep track of running child processes @@ -293,19 +299,19 @@ export class RMarkdownPreviewManager { resourceViewColumn, outputUri, fileUri, - dir + htmlContent, + this ); - this.childStore.add(childProcess); - const html = await this.loadHtml(childProcess); - panel.webview.html = html; - childProcess.startFileWatcher(this); + this.childStore.add(fileUri, childProcess); + + panel.webview.html = this.getHtmlContent(childProcess); // state change panel.onDidDispose(() => { // clear values this.activePreview === childProcess ? undefined : this.activePreview; void setContext('r.preview.active', false); - this.childStore.delete(childProcess); + this.childStore.delete(fileUri); }); panel.onDidChangeViewState(({ webviewPanel }) => { @@ -316,13 +322,36 @@ export class RMarkdownPreviewManager { }); } - private async loadHtml(childProcess: RMarkdownChild): Promise { - const content = await readContent(childProcess.outputUri.path, 'utf8'); - const html = content.replace('', '') - .replace(/<(\w+)\s+(href|src)="(?!\w+:)/g, - `<$1 $2="${String(childProcess.panel.webview.asWebviewUri(vscode.Uri.file(childProcess.dir)))}/`); - return html; + private getHtmlContent(childProcess: RMarkdownChild): string { + if (!this.vscodeTheme) { + return childProcess.htmlContent.replace(/<(\w+)\s+(href|src)="(?!\w+:)/g, + `<$1 $2="${String(childProcess.panel.webview.asWebviewUri(vscode.Uri.file(tmpDir)))}/`); + } else { + // todo, potentially emulate vscode syntax highlighting? + const style = + ` + `; + const headerReg = /(?<=)[\s\S]*(?=<\/head>)/g; + const header = headerReg.exec(childProcess.htmlContent)?.[0]; + const html = childProcess.htmlContent + .replace(header, header + style) + .replace(/<(\w+)\s+(href|src)="(?!\w+:)/g, + `<$1 $2="${String(childProcess.panel.webview.asWebviewUri(vscode.Uri.file(tmpDir)))}/`); + return html; + } } - } diff --git a/src/session.ts b/src/session.ts index 3036a4be5..e6d3a5e07 100644 --- a/src/session.ts +++ b/src/session.ts @@ -231,7 +231,7 @@ export async function showBrowser(url: string, title: string, viewer: string | b console.info('[showBrowser] Done'); } -export function getBrowserHtml(uri: Uri): string { +function getBrowserHtml(uri: Uri): string { return ` From a5e15d3946f6ad64260a40091a92e2563cf3256b Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:34:31 +1000 Subject: [PATCH 14/21] Fix lint --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index c0223c82a..c0ebdd61f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -96,7 +96,7 @@ export async function activate(context: vscode.ExtensionContext): Promise rMarkdownPreview.previewRmd(vscode.ViewColumn.Beside), 'r.markdown.showPreview': (uri: vscode.Uri) => rMarkdownPreview.previewRmd(vscode.ViewColumn.Active, uri), 'r.markdown.preview.refresh': () => rMarkdownPreview.refreshPanel(), - 'r.markdown.preview.openExternal': async () => await rMarkdownPreview.openExternalBrowser(), + 'r.markdown.preview.openExternal': () => rMarkdownPreview.openExternalBrowser(), 'r.markdown.preview.showSource': () => rMarkdownPreview.showSource(), 'r.markdown.preview.toggleStyle': () => rMarkdownPreview.toggleTheme(), From d8053b7aea6bd2285d62770e7470bf64f3fcef47 Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 8 Jul 2021 11:56:41 +1000 Subject: [PATCH 15/21] Restructure codebase, some bug fixes, styling - Fix bug that prevented context menu preview from working if a document wasn't open - Rename classes to better explain their purpose - RMarkdownPreview now handles its html content, rather than manager - Use cheerio for styling - Move escapeHtml to utils --- src/helpViewer/index.ts | 14 +-- src/rmarkdown/preview.ts | 224 +++++++++++++++++++++------------------ src/util.ts | 13 +++ 3 files changed, 135 insertions(+), 116 deletions(-) diff --git a/src/helpViewer/index.ts b/src/helpViewer/index.ts index 86249a85e..ae0facd32 100644 --- a/src/helpViewer/index.ts +++ b/src/helpViewer/index.ts @@ -6,7 +6,7 @@ import * as hljs from 'highlight.js'; import * as api from '../api'; -import { config, getRpath, doWithProgress, DummyMemento, getRPathConfigEntry } from '../util'; +import { config, getRpath, doWithProgress, DummyMemento, getRPathConfigEntry, escapeHtml } from '../util'; import { HelpPanel } from './panel'; import { HelpProvider, AliasProvider } from './helpProvider'; import { HelpTreeWrapper } from './treeView'; @@ -524,15 +524,3 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer(Object.entries({ - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '/': '/' - })); - return String(source).replace(/[&<>"'/]/g, (s: string) => entityMap.get(s) || ''); -} diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 6ff527b1b..851615311 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -2,24 +2,24 @@ import * as cp from 'child_process'; import * as vscode from 'vscode'; import * as fs from 'fs-extra'; - -import { config, doWithProgress, getRpath, readContent, setContext } from '../util'; -import { extensionContext, tmpDir } from '../extension'; +import * as cheerio from 'cheerio'; import path = require('path'); import crypto = require('crypto'); -class RMarkdownChild extends vscode.Disposable { +import { config, doWithProgress, getRpath, readContent, setContext, escapeHtml } from '../util'; +import { extensionContext, tmpDir } from '../extension'; + +class RMarkdownPreview extends vscode.Disposable { title: string; cp: cp.ChildProcessWithoutNullStreams; panel: vscode.WebviewPanel; resourceViewColumn: vscode.ViewColumn; - // todo, restructure, as uri is now used as a key - uri: vscode.Uri outputUri: vscode.Uri; - htmlContent: string; + htmlDarkContent: string; + htmlLightContent: string; fileWatcher: fs.FSWatcher; - constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, uri: vscode.Uri, htmlContent: string, RMarkdownPreviewManager: RMarkdownPreviewManager) { + constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, uri: vscode.Uri, RMarkdownPreviewManager: RMarkdownPreviewManager, themeBool: boolean) { super(() => { this.cp?.kill('SIGKILL'); this.panel?.dispose(); @@ -31,15 +31,27 @@ class RMarkdownChild extends vscode.Disposable { this.cp = cp; this.panel = panel; this.resourceViewColumn = resourceViewColumn; - this.uri = uri; this.outputUri = outputUri; - this.htmlContent = htmlContent; - this.startFileWatcher(RMarkdownPreviewManager); + void this.refreshContent(themeBool); + this.startFileWatcher(RMarkdownPreviewManager, uri); + } + + public styleHtml(themeBool: boolean) { + if (themeBool) { + this.panel.webview.html = this.htmlDarkContent; + } else { + this.panel.webview.html = this.htmlLightContent; + } + } + + public async refreshContent(themeBool: boolean) { + this.getHtmlContent(await readContent(this.outputUri.path, 'utf8')); + this.styleHtml(themeBool); } - private startFileWatcher(RMarkdownPreviewManager: RMarkdownPreviewManager) { + private startFileWatcher(RMarkdownPreviewManager: RMarkdownPreviewManager, uri: vscode.Uri) { let fsTimeout: NodeJS.Timeout; - const fileWatcher = fs.watch(this.uri.path, {}, () => { + const fileWatcher = fs.watch(uri.path, {}, () => { if (!fsTimeout) { fsTimeout = setTimeout(() => { fsTimeout = null; }, 1000); void RMarkdownPreviewManager.updatePreview(this); @@ -47,22 +59,59 @@ class RMarkdownChild extends vscode.Disposable { }); this.fileWatcher = fileWatcher; } + + private getHtmlContent(htmlContent: string): void { + let content = htmlContent.replace(/<(\w+)\s+(href|src)="(?!\w+:)/g, + `<$1 $2="${String(this.panel.webview.asWebviewUri(vscode.Uri.file(tmpDir)))}/`); + this.htmlLightContent = content; + + const re = new RegExp('.*', 'ms'); + const isHtml = !!re.exec(content); + + if (!isHtml) { + const html = escapeHtml(content); + content = `
${html}
`; + } + + const $ = cheerio.load(this.htmlLightContent); + $('head style').remove(); + + // todo, potentially emulate vscode syntax highlighting? + const style = + ` + `; + $('head').append(style); + this.htmlDarkContent = $.html(); + } } -class RMarkdownChildStore extends vscode.Disposable { - private store: Map = new Map(); +class RMarkdownPreviewStore extends vscode.Disposable { + private store: Map = new Map(); constructor() { super((): void => { - for (const child of this.store) { - child[1].dispose(); + for (const preview of this.store) { + preview[1].dispose(); } this.store.clear(); }); } - public add(uri:vscode.Uri, child: RMarkdownChild): Map { - return this.store.set(uri, child); + public add(uri: vscode.Uri, preview: RMarkdownPreview): Map { + return this.store.set(uri, preview); } // dispose child and remove it from set @@ -71,10 +120,19 @@ class RMarkdownChildStore extends vscode.Disposable { return this.store.delete(uri); } - public get(uri: vscode.Uri): RMarkdownChild { + public get(uri: vscode.Uri): RMarkdownPreview { return this.store.get(uri); } + public getUri(preview: RMarkdownPreview): vscode.Uri { + for (const _preview of this.store) { + if (_preview[1] === preview) { + return _preview[0]; + } + } + return undefined; + } + public has(uri: vscode.Uri): boolean { return this.store.has(uri); } @@ -89,9 +147,9 @@ export class RMarkdownPreviewManager { private rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); // the currently selected RMarkdown preview - private activePreview: RMarkdownChild; + private activePreview: { uri: vscode.Uri, preview: RMarkdownPreview } = { uri: null, preview: null}; // store of all open RMarkdown previews - private childStore: RMarkdownChildStore = new RMarkdownChildStore; + 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(); @@ -101,18 +159,17 @@ export class RMarkdownPreviewManager { public async init(): Promise { this.rPath = await getRpath(false); - extensionContext.subscriptions.push(this.childStore); + extensionContext.subscriptions.push(this.previewStore); } public async previewRmd(viewer: vscode.ViewColumn, uri?: vscode.Uri): Promise { const fileUri = uri ?? vscode.window.activeTextEditor.document.uri; const fileName = fileUri.path.substring(fileUri.path.lastIndexOf('/') + 1); - const currentViewColumn: vscode.ViewColumn = vscode.window.activeTextEditor.viewColumn ?? vscode.ViewColumn.Active; - + const currentViewColumn: vscode.ViewColumn = vscode.window.activeTextEditor?.viewColumn ?? vscode.ViewColumn.Active ?? vscode.ViewColumn.One; if (this.busyUriStore.has(fileUri)) { return; - } else if (this.childStore.has(fileUri)) { - this.childStore.get(fileUri)?.panel.reveal(); + } else if (this.previewStore.has(fileUri)) { + this.previewStore.get(fileUri)?.panel.reveal(); } else { this.busyUriStore.add(fileUri); await this.knitWithProgress(fileUri, fileName, viewer, currentViewColumn, uri); @@ -120,20 +177,18 @@ export class RMarkdownPreviewManager { } } - // todo, should this trigger a re-knit? - public refreshPanel(child?: RMarkdownChild): void { - if (child) { - child.panel.webview.html = this.getHtmlContent(child); + public refreshPanel(preview?: RMarkdownPreview): void { + if (preview) { + void preview.refreshContent(this.vscodeTheme); } else if (this.activePreview) { - this.activePreview.panel.webview.html = ''; - this.activePreview.panel.webview.html = this.getHtmlContent(this.activePreview); + void this.activePreview?.preview?.refreshContent(this.vscodeTheme); } } public toggleTheme(): void { this.vscodeTheme = !this.vscodeTheme; - for (const child of this.childStore) { - this.refreshPanel(child[1]); + for (const preview of this.previewStore) { + void preview[1].styleHtml(this.vscodeTheme); } } @@ -147,10 +202,10 @@ export class RMarkdownPreviewManager { // (e.g., is an unopened tab) public async showSource(): Promise { if (this.activePreview) { - await vscode.commands.executeCommand('vscode.open', this.activePreview.uri, { + await vscode.commands.executeCommand('vscode.open', this.activePreview?.uri, { preserveFocus: false, preview: false, - viewColumn: this.activePreview.resourceViewColumn ?? this.activePreview.panel.viewColumn ?? vscode.ViewColumn.Active + viewColumn: this.activePreview?.preview?.resourceViewColumn ?? this.activePreview?.preview?.panel.viewColumn ?? vscode.ViewColumn.Active }); } } @@ -158,11 +213,28 @@ export class RMarkdownPreviewManager { public openExternalBrowser(): void { if (this.activePreview) { void vscode.env.openExternal( - this.activePreview.outputUri + this.activePreview?.preview?.outputUri ); } } + public async updatePreview(preview: RMarkdownPreview): Promise { + const previewUri = this.previewStore?.getUri(preview); + preview.cp?.kill('SIGKILL'); + + const childProcess: cp.ChildProcessWithoutNullStreams | void = await this.knitDocument(previewUri, preview.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) { + preview.cp = childProcess; + } + + this.refreshPanel(preview); + } + private async knitWithProgress(fileUri: vscode.Uri, fileName: string, viewer: vscode.ViewColumn, currentViewColumn: vscode.ViewColumn, uri?: vscode.Uri) { await doWithProgress(async (token: vscode.CancellationToken) => { await this.knitDocument(fileUri, fileName, token, viewer, currentViewColumn); @@ -182,8 +254,8 @@ export class RMarkdownPreviewManager { } // this can occur when a successfuly knitted document is later altered (while still being previewed) // and subsequently fails to knit - if (this.childStore.has(uri)) { - this.childStore.delete(uri); + if (this.previewStore.has(uri)) { + this.previewStore.delete(uri); } else { rejection.cp.kill('SIGKILL'); } @@ -252,27 +324,7 @@ export class RMarkdownPreviewManager { }); } - public async updatePreview(child: RMarkdownChild): Promise { - child.cp?.kill('SIGKILL'); - - const childProcess: cp.ChildProcessWithoutNullStreams | void = await this.knitDocument(child.uri, child.title).catch((rejection: { - cp: cp.ChildProcessWithoutNullStreams, - wasCancelled?: boolean - }) => { - void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); - this.rMarkdownOutput.show(true); - rejection.cp.kill('SIGINT'); - this.childStore.get(child.uri)?.dispose(); - }); - - if (childProcess) { - child.cp = childProcess; - } - - this.refreshPanel(child); - } - - private async openPreview(outputUri: vscode.Uri, fileUri: vscode.Uri, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn): Promise { + private openPreview(outputUri: vscode.Uri, fileUri: vscode.Uri, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn): void { const panel = vscode.window.createWebviewPanel( 'previewRmd', `Preview ${title}`, @@ -286,72 +338,38 @@ export class RMarkdownPreviewManager { retainContextWhenHidden: true, localResourceRoots: [vscode.Uri.file(tmpDir)], }); - const htmlContent = await readContent(outputUri.path, 'utf8'); // 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 // general state tracking) - const childProcess = new RMarkdownChild( + const preview = new RMarkdownPreview( title, cp, panel, resourceViewColumn, outputUri, fileUri, - htmlContent, - this + this, + this.vscodeTheme ); - this.childStore.add(fileUri, childProcess); - - panel.webview.html = this.getHtmlContent(childProcess); + this.previewStore.add(fileUri, preview); // state change panel.onDidDispose(() => { // clear values - this.activePreview === childProcess ? undefined : this.activePreview; + this.activePreview = this.activePreview?.preview === preview ? { uri: null, preview: null} : this.activePreview; void setContext('r.preview.active', false); - this.childStore.delete(fileUri); + this.previewStore.delete(fileUri); }); panel.onDidChangeViewState(({ webviewPanel }) => { void setContext('r.preview.active', webviewPanel.active); if (webviewPanel.active) { - this.activePreview = childProcess; + this.activePreview.preview = preview; + this.activePreview.uri = fileUri; } }); } - - private getHtmlContent(childProcess: RMarkdownChild): string { - if (!this.vscodeTheme) { - return childProcess.htmlContent.replace(/<(\w+)\s+(href|src)="(?!\w+:)/g, - `<$1 $2="${String(childProcess.panel.webview.asWebviewUri(vscode.Uri.file(tmpDir)))}/`); - } else { - // todo, potentially emulate vscode syntax highlighting? - const style = - ` - `; - const headerReg = /(?<=)[\s\S]*(?=<\/head>)/g; - const header = headerReg.exec(childProcess.htmlContent)?.[0]; - const html = childProcess.htmlContent - .replace(header, header + style) - .replace(/<(\w+)\s+(href|src)="(?!\w+:)/g, - `<$1 $2="${String(childProcess.panel.webview.asWebviewUri(vscode.Uri.file(tmpDir)))}/`); - return html; - } - } } diff --git a/src/util.ts b/src/util.ts index b9cac6a22..d649dd4e8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -339,3 +339,16 @@ export async function setContext(key: string, value: any): Promise { 'setContext', key, value ); } + +// Helper function used to convert raw text files to html +export function escapeHtml(source: string): string { + const entityMap = new Map(Object.entries({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/' + })); + return String(source).replace(/[&<>"'/]/g, (s: string) => entityMap.get(s) || ''); +} From 2d6596acc2a5719d17f618f6d5da02a9c1879c0b Mon Sep 17 00:00:00 2001 From: "Elian H. Thiele-Evans" <60372411+ElianHugh@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:41:59 +1000 Subject: [PATCH 16/21] Style changes Improve styling so as to not create unreadable text --- src/extension.ts | 2 +- src/rmarkdown/preview.ts | 40 +++++++++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index c0ebdd61f..c0223c82a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -96,7 +96,7 @@ export async function activate(context: vscode.ExtensionContext): Promise rMarkdownPreview.previewRmd(vscode.ViewColumn.Beside), 'r.markdown.showPreview': (uri: vscode.Uri) => rMarkdownPreview.previewRmd(vscode.ViewColumn.Active, uri), 'r.markdown.preview.refresh': () => rMarkdownPreview.refreshPanel(), - 'r.markdown.preview.openExternal': () => rMarkdownPreview.openExternalBrowser(), + 'r.markdown.preview.openExternal': async () => await rMarkdownPreview.openExternalBrowser(), 'r.markdown.preview.showSource': () => rMarkdownPreview.showSource(), 'r.markdown.preview.toggleStyle': () => rMarkdownPreview.toggleTheme(), diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 851615311..47493f051 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -3,9 +3,11 @@ import * as cp from 'child_process'; import * as vscode from 'vscode'; import * as fs from 'fs-extra'; import * as cheerio from 'cheerio'; + import path = require('path'); import crypto = require('crypto'); + import { config, doWithProgress, getRpath, readContent, setContext, escapeHtml } from '../util'; import { extensionContext, tmpDir } from '../extension'; @@ -74,23 +76,34 @@ class RMarkdownPreview extends vscode.Disposable { } const $ = cheerio.load(this.htmlLightContent); - $('head style').remove(); + const chunkCol = String(config().get('rmarkdown.chunkBackgroundColor')); + + // make the output chunks a little lighter to stand out + 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})`; - // todo, potentially emulate vscode syntax highlighting? const style = ` `; $('head').append(style); @@ -212,9 +225,14 @@ export class RMarkdownPreviewManager { public openExternalBrowser(): void { if (this.activePreview) { - void vscode.env.openExternal( - this.activePreview?.preview?.outputUri - ); + // void open( + // this.activePreview.preview.outputUri.fsPath, + // { + // app: { + // name: 'firefox' + // } + // } + // ); } } @@ -270,7 +288,7 @@ export class RMarkdownPreviewManager { const cmd = ( `${this.rPath} --silent --slave --no-save --no-restore -e ` + `"cat('${lim}', - rmarkdown::render('${String(fileUri.path)}', output_format = rmarkdown::html_document(), output_file = '${outputFile}'), + rmarkdown::render('${String(fileUri.path)}', output_format = rmarkdown::html_document(), output_file = '${outputFile}', intermediates_dir = '${tmpDir}'), '${lim}', sep='')"` ); From f47ad8e8fb0983f7da9061919d5304af20f6ae7f Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 9 Jul 2021 14:35:20 +0800 Subject: [PATCH 17/21] Minor fixes --- src/extension.ts | 2 +- src/rmarkdown/preview.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index c0223c82a..67b43c6b3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -96,7 +96,7 @@ export async function activate(context: vscode.ExtensionContext): Promise rMarkdownPreview.previewRmd(vscode.ViewColumn.Beside), 'r.markdown.showPreview': (uri: vscode.Uri) => rMarkdownPreview.previewRmd(vscode.ViewColumn.Active, uri), 'r.markdown.preview.refresh': () => rMarkdownPreview.refreshPanel(), - 'r.markdown.preview.openExternal': async () => await rMarkdownPreview.openExternalBrowser(), + 'r.markdown.preview.openExternal': () => void rMarkdownPreview.openExternalBrowser(), 'r.markdown.preview.showSource': () => rMarkdownPreview.showSource(), 'r.markdown.preview.toggleStyle': () => rMarkdownPreview.toggleTheme(), diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 47493f051..37219d683 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -71,7 +71,7 @@ class RMarkdownPreview extends vscode.Disposable { const isHtml = !!re.exec(content); if (!isHtml) { - const html = escapeHtml(content); + const html: string = escapeHtml(content); content = `
${html}
`; } From 30a8effb621d525d6978bc282140a283191ec908 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 9 Jul 2021 14:38:42 +0800 Subject: [PATCH 18/21] Update openExternalBrowser --- src/rmarkdown/preview.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 37219d683..ca67ba96c 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -223,16 +223,9 @@ export class RMarkdownPreviewManager { } } - public openExternalBrowser(): void { + public async openExternalBrowser(): Promise { if (this.activePreview) { - // void open( - // this.activePreview.preview.outputUri.fsPath, - // { - // app: { - // name: 'firefox' - // } - // } - // ); + await vscode.env.openExternal(this.activePreview?.preview?.outputUri); } } From b55abef14ce9bef2b7027d8d7a275a3c1898e948 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 9 Jul 2021 17:32:14 +0800 Subject: [PATCH 19/21] No openPreview if cancelled --- src/rmarkdown/preview.ts | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index ca67ba96c..84e843652 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -71,7 +71,7 @@ class RMarkdownPreview extends vscode.Disposable { const isHtml = !!re.exec(content); if (!isHtml) { - const html: string = escapeHtml(content); + const html = escapeHtml(content); content = `
${html}
`; } @@ -299,19 +299,23 @@ export class RMarkdownPreviewManager { (data: Buffer) => { const dat = data.toString('utf8'); this.rMarkdownOutput.appendLine(dat); - const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); - if (outputUrl) { - if (viewer !== undefined) { - void this.openPreview( - vscode.Uri.parse(outputUrl), - fileUri, - fileName, - childProcess, - viewer, - currentViewColumn - ); - } + if (token?.isCancellationRequested) { resolve(childProcess); + } else { + const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); + if (outputUrl) { + if (viewer !== undefined) { + void this.openPreview( + vscode.Uri.parse(outputUrl), + fileUri, + fileName, + childProcess, + viewer, + currentViewColumn + ); + } + resolve(childProcess); + } } } ); From 0ff17d07a229f89b8b681b7420d2066060586f46 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 9 Jul 2021 20:56:38 +0800 Subject: [PATCH 20/21] Add enable/disable auto refresh command --- package.json | 25 +++++++++++++++++++++++++ src/extension.ts | 2 ++ src/rmarkdown/preview.ts | 36 +++++++++++++++++++++++++++++++----- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d190e097b..2f0612dbf 100644 --- a/package.json +++ b/package.json @@ -562,6 +562,16 @@ "command": "r.markdown.preview.toggleStyle", "icon": "$(symbol-color)" }, + { + "title": "Enable Auto Refresh", + "category": "R", + "command": "r.markdown.preview.enableAutoRefresh" + }, + { + "title": "Disable Auto Refresh", + "category": "R", + "command": "r.markdown.preview.disableAutoRefresh" + }, { "title": "Launch RStudio Addin", "category": "R", @@ -880,6 +890,16 @@ "when": "resourceScheme =~ /webview/ && r.preview.active", "group": "1_rMarkdown" }, + { + "command": "r.markdown.preview.enableAutoRefresh", + "when": "resourceScheme =~ /webview/ && r.preview.active && !r.preview.autoRefresh", + "group": "1_rMarkdown" + }, + { + "command": "r.markdown.preview.disableAutoRefresh", + "when": "resourceScheme =~ /webview/ && r.preview.active && r.preview.autoRefresh", + "group": "1_rMarkdown" + }, { "command": "r.browser.refresh", "when": "resourceScheme =~ /webview/ && r.browser.active", @@ -1189,6 +1209,11 @@ "default": "rgba(128, 128, 128, 0.1)", "description": "RMarkdown chunk background color in RGBA or RGB value. Defaults to rgba(128, 128, 128, 0.1). Leave it empty to disable it (use default editor background color). Reload VS Code after changing settings.\n\nLearn how to set colors: https://www.w3schools.com/css/css_colors_rgb.asp.\n\nExamples for syntax rgba(, , , ):\nrgba(128, 128, 128, 0.1)\nrgba(128, 128, 128, 0.3)\nrgba(255, 165, 0, 0.1)\n\n" }, + "r.rmarkdown.preview.autoRefresh": { + "type": "boolean", + "default": true, + "description": "Enable automatic refresh of R Markdown preview on file update." + }, "r.helpPanel.enableSyntaxHighlighting": { "type": "boolean", "default": true, diff --git a/src/extension.ts b/src/extension.ts index 67b43c6b3..c287d76d2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -99,6 +99,8 @@ export async function activate(context: vscode.ExtensionContext): Promise void rMarkdownPreview.openExternalBrowser(), 'r.markdown.preview.showSource': () => rMarkdownPreview.showSource(), 'r.markdown.preview.toggleStyle': () => rMarkdownPreview.toggleTheme(), + 'r.markdown.preview.enableAutoRefresh': () => rMarkdownPreview.enableAutoRefresh(), + 'r.markdown.preview.disableAutoRefresh': () => rMarkdownPreview.disableAutoRefresh(), // editor independent commands 'r.createGitignore': rGitignore.createGitignore, diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 84e843652..712c7eb5f 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -20,8 +20,11 @@ class RMarkdownPreview extends vscode.Disposable { htmlDarkContent: string; htmlLightContent: string; fileWatcher: fs.FSWatcher; + autoRefresh: boolean; - constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, uri: vscode.Uri, RMarkdownPreviewManager: RMarkdownPreviewManager, themeBool: boolean) { + constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, + resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, uri: vscode.Uri, + RMarkdownPreviewManager: RMarkdownPreviewManager, themeBool: boolean, autoRefresh: boolean) { super(() => { this.cp?.kill('SIGKILL'); this.panel?.dispose(); @@ -34,6 +37,7 @@ class RMarkdownPreview extends vscode.Disposable { this.panel = panel; this.resourceViewColumn = resourceViewColumn; this.outputUri = outputUri; + this.autoRefresh = autoRefresh; void this.refreshContent(themeBool); this.startFileWatcher(RMarkdownPreviewManager, uri); } @@ -54,7 +58,7 @@ class RMarkdownPreview extends vscode.Disposable { private startFileWatcher(RMarkdownPreviewManager: RMarkdownPreviewManager, uri: vscode.Uri) { let fsTimeout: NodeJS.Timeout; const fileWatcher = fs.watch(uri.path, {}, () => { - if (!fsTimeout) { + if (this.autoRefresh && !fsTimeout) { fsTimeout = setTimeout(() => { fsTimeout = null; }, 1000); void RMarkdownPreviewManager.updatePreview(this); } @@ -198,6 +202,24 @@ export class RMarkdownPreviewManager { } } + public enableAutoRefresh(preview?: RMarkdownPreview): void { + if (preview) { + preview.autoRefresh = true; + } else if (this.activePreview?.preview) { + this.activePreview.preview.autoRefresh = true; + void setContext('r.preview.autoRefresh', true); + } + } + + public disableAutoRefresh(preview?: RMarkdownPreview): void { + if (preview) { + preview.autoRefresh = false; + } else if (this.activePreview?.preview) { + this.activePreview.preview.autoRefresh = false; + void setContext('r.preview.autoRefresh', false); + } + } + public toggleTheme(): void { this.vscodeTheme = !this.vscodeTheme; for (const preview of this.previewStore) { @@ -305,13 +327,15 @@ export class RMarkdownPreviewManager { 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), fileUri, fileName, childProcess, viewer, - currentViewColumn + currentViewColumn, + autoRefresh ); } resolve(childProcess); @@ -339,7 +363,7 @@ export class RMarkdownPreviewManager { }); } - private openPreview(outputUri: vscode.Uri, fileUri: vscode.Uri, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn): void { + private openPreview(outputUri: vscode.Uri, fileUri: vscode.Uri, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn, autoRefresh:boolean): void { const panel = vscode.window.createWebviewPanel( 'previewRmd', `Preview ${title}`, @@ -366,7 +390,8 @@ export class RMarkdownPreviewManager { outputUri, fileUri, this, - this.vscodeTheme + this.vscodeTheme, + autoRefresh ); this.previewStore.add(fileUri, preview); @@ -383,6 +408,7 @@ export class RMarkdownPreviewManager { if (webviewPanel.active) { this.activePreview.preview = preview; this.activePreview.uri = fileUri; + void setContext('r.preview.autoRefresh', preview.autoRefresh); } }); } From e42d34f9051e9955dbf055dba2ae97f981fb9dea Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 9 Jul 2021 21:32:10 +0800 Subject: [PATCH 21/21] Update toolbar --- package.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2f0612dbf..8aad6fdd0 100644 --- a/package.json +++ b/package.json @@ -565,12 +565,14 @@ { "title": "Enable Auto Refresh", "category": "R", - "command": "r.markdown.preview.enableAutoRefresh" + "command": "r.markdown.preview.enableAutoRefresh", + "icon": "$(sync)" }, { "title": "Disable Auto Refresh", "category": "R", - "command": "r.markdown.preview.disableAutoRefresh" + "command": "r.markdown.preview.disableAutoRefresh", + "icon": "$(sync-ignored)" }, { "title": "Launch RStudio Addin", @@ -888,17 +890,17 @@ { "command": "r.markdown.preview.refresh", "when": "resourceScheme =~ /webview/ && r.preview.active", - "group": "1_rMarkdown" + "group": "navigation@4" }, { "command": "r.markdown.preview.enableAutoRefresh", "when": "resourceScheme =~ /webview/ && r.preview.active && !r.preview.autoRefresh", - "group": "1_rMarkdown" + "group": "navigation@5" }, { "command": "r.markdown.preview.disableAutoRefresh", "when": "resourceScheme =~ /webview/ && r.preview.active && r.preview.autoRefresh", - "group": "1_rMarkdown" + "group": "navigation@5" }, { "command": "r.browser.refresh",