From e2d8746c94e2a96af5e87dd176a03094b82be77a Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Wed, 9 Feb 2022 13:19:18 +0800 Subject: [PATCH 01/14] Add templates.R --- R/rmarkdown/templates.R | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 R/rmarkdown/templates.R diff --git a/R/rmarkdown/templates.R b/R/rmarkdown/templates.R new file mode 100644 index 000000000..19876e41c --- /dev/null +++ b/R/rmarkdown/templates.R @@ -0,0 +1,28 @@ +requireNamespace("jsonlite") +requireNamespace("yaml") + +pkgs <- .packages(all.available = TRUE) +templates <- new.env() +template_dirs <- lapply(pkgs, function(pkg) { + dir <- system.file("rmarkdown/templates", package = pkg) + if (dir.exists(dir)) { + ids <- list.dirs(dir, full.names = FALSE, recursive = FALSE) + for (id in ids) { + file <- file.path(dir, id, "template.yaml") + if (file.exists(file)) { + data <- yaml::read_yaml(file) + data$id <- id + data$package <- pkg + templates[[paste0(pkg, "::", id)]] <- data + } + } + } +}) + +template_list <- unname(as.list(templates)) + +lim <- "---vsc---" + +json <- jsonlite::toJSON(template_list, auto_unbox = TRUE) + +cat(lim, json, lim, "\n", sep = "") From 04aa9c18d7d51cc88009a81e737e05a669c7cc4a Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Wed, 9 Feb 2022 14:51:22 +0800 Subject: [PATCH 02/14] Implement newDraft command --- R/rmarkdown/templates.R | 7 +- package.json | 5 ++ src/extension.ts | 1 + src/rmarkdown/draft.ts | 144 ++++++++++++++++++++++++++++++++++++++++ src/rmarkdown/index.ts | 1 + 5 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 src/rmarkdown/draft.ts diff --git a/R/rmarkdown/templates.R b/R/rmarkdown/templates.R index 19876e41c..73114f7f0 100644 --- a/R/rmarkdown/templates.R +++ b/R/rmarkdown/templates.R @@ -20,9 +20,6 @@ template_dirs <- lapply(pkgs, function(pkg) { }) template_list <- unname(as.list(templates)) +file <- Sys.getenv("VSCR_FILE") -lim <- "---vsc---" - -json <- jsonlite::toJSON(template_list, auto_unbox = TRUE) - -cat(lim, json, lim, "\n", sep = "") +jsonlite::write_json(template_list, file, auto_unbox = TRUE) diff --git a/package.json b/package.json index 0cd9eb746..54a299667 100644 --- a/package.json +++ b/package.json @@ -537,6 +537,11 @@ "category": "R", "command": "r.goToNextChunk" }, + { + "title": "New Draft", + "category": "R Markdown", + "command": "r.rmarkdown.newDraft" + }, { "command": "r.rmarkdown.setKnitDirectory", "title": "R: Set Knit directory", diff --git a/src/extension.ts b/src/extension.ts index c10544815..605e96407 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -93,6 +93,7 @@ export async function activate(context: vscode.ExtensionContext): Promise rmarkdown.newDraft(), 'r.rmarkdown.setKnitDirectory': () => rmdKnitManager.setKnitDir(), 'r.rmarkdown.showPreviewToSide': () => rmdPreviewManager.previewRmd(vscode.ViewColumn.Beside), 'r.rmarkdown.showPreview': (uri: vscode.Uri) => rmdPreviewManager.previewRmd(vscode.ViewColumn.Active, uri), diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts new file mode 100644 index 000000000..40e877865 --- /dev/null +++ b/src/rmarkdown/draft.ts @@ -0,0 +1,144 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +import { QuickPickItem, QuickPickOptions, window, workspace } from 'vscode'; +import { extensionContext } from '../extension'; +import { getCurrentWorkspaceFolder, getRpath } from '../util'; +import * as cp from 'child_process'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import { readJSON } from 'fs-extra'; + +interface TemplateItem extends QuickPickItem { + id: string; + package: string; +} + +async function getTemplateItems(cwd: string): Promise { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-')); + const tempFile = path.join(tempDir, 'templates.json'); + const rPath = await getRpath(); + const options: cp.ExecSyncOptionsWithStringEncoding = { + cwd: cwd, + encoding: 'utf-8', + env: { + ...process.env, + VSCR_FILE: tempFile + } + }; + + const args = [ + '--silent', + '--slave', + '--no-save', + '--no-restore', + '-f', + extensionContext.asAbsolutePath('R/rmarkdown/draft.R') + ]; + + try { + const result = cp.spawnSync(rPath, args, options); + if (result.error) { + throw result.error; + } + + const templates: any[] = await readJSON(tempFile).then( + (result) => result, + () => { + throw ('Failed to load templates from installed packages.'); + } + ); + + const items = templates.map((x) => { + return { + alwaysShow: false, + description: `{${x.package}}`, + label: x.name, + detail: x.description, + picked: false, + package: x.package, + id: x.id, + }; + }); + + return items; + } catch (e) { + void window.showErrorMessage((<{ message: string }>e).message); + } finally { + fs.rmdirSync(tempDir, { recursive: true }); + } +} + +async function launchTemplatePicker(cwd: string): Promise { + const options: QuickPickOptions = { + matchOnDescription: true, + matchOnDetail: true, + canPickMany: false, + ignoreFocusOut: false, + placeHolder: '', + onDidSelectItem: undefined + }; + + const items = await getTemplateItems(cwd); + + const selection: TemplateItem = await window.showQuickPick(items, options); + return selection; +} + +async function makeDraft(template: TemplateItem, cwd: string): Promise { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-')); + const tempFile = path.join(tempDir, 'draft.Rmd'); + const rPath = await getRpath(); + const options: cp.ExecSyncOptionsWithStringEncoding = { + cwd: cwd, + encoding: 'utf-8', + }; + + const args = [ + '--silent', + '--slave', + '--no-save', + '--no-restore', + '-e', + `rmarkdown::draft(file='${tempFile}', template='${template.id}', package='${template.package}', edit=FALSE)`, + ]; + + try { + const result = cp.spawnSync(rPath, args, options); + if (result.error) { + throw result.error; + } + + if (fs.existsSync(tempFile)) { + const text = fs.readFileSync(tempFile, 'utf-8'); + return text; + } else { + throw new Error('Failed to create draft.'); + } + } catch (e) { + void window.showErrorMessage((<{ message: string }>e).message); + } finally { + fs.rmdirSync(tempDir, { recursive: true }); + } + + return undefined; +} + +export async function newDraft(): Promise { + const cwd = getCurrentWorkspaceFolder()?.uri.fsPath ?? os.homedir(); + const template = await launchTemplatePicker(cwd); + if (!template) { + return; + } + + const text = await makeDraft(template, cwd); + if (text) { + void workspace.openTextDocument({ language: 'rmd', content: text }); + } +} diff --git a/src/rmarkdown/index.ts b/src/rmarkdown/index.ts index 438ad45bd..5bfcb69c0 100644 --- a/src/rmarkdown/index.ts +++ b/src/rmarkdown/index.ts @@ -5,6 +5,7 @@ import { config } from '../util'; // reexports export { knitDir, RMarkdownKnitManager } from './knit'; export { RMarkdownPreviewManager } from './preview'; +export { newDraft } from './draft'; function isRDocument(document: vscode.TextDocument) { return (document.languageId === 'r'); From 5d9365dfc1f1ff80dd297862f2b4461309ba3bd9 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Wed, 9 Feb 2022 18:38:57 +0800 Subject: [PATCH 03/14] Fix path --- src/rmarkdown/draft.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 40e877865..9397ef063 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -39,7 +39,7 @@ async function getTemplateItems(cwd: string): Promise { '--no-save', '--no-restore', '-f', - extensionContext.asAbsolutePath('R/rmarkdown/draft.R') + extensionContext.asAbsolutePath('R/rmarkdown/templates.R') ]; try { From 7abf2dabf7cdccf4838105c0fb0ce8a4673facf8 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Wed, 9 Feb 2022 20:05:04 +0800 Subject: [PATCH 04/14] Update draft --- src/rmarkdown/draft.ts | 66 +++++++++++++++--------------------------- src/util.ts | 15 ++++++---- 2 files changed, 33 insertions(+), 48 deletions(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 9397ef063..8d287fe7b 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -6,14 +6,15 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { QuickPickItem, QuickPickOptions, window, workspace } from 'vscode'; +import { QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; import { extensionContext } from '../extension'; -import { getCurrentWorkspaceFolder, getRpath } from '../util'; +import { executeRCommand, getCurrentWorkspaceFolder, getRpath } from '../util'; import * as cp from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; import { readJSON } from 'fs-extra'; +import { join } from 'path'; interface TemplateItem extends QuickPickItem { id: string; @@ -91,43 +92,12 @@ async function launchTemplatePicker(cwd: string): Promise { return selection; } -async function makeDraft(template: TemplateItem, cwd: string): Promise { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-')); - const tempFile = path.join(tempDir, 'draft.Rmd'); - const rPath = await getRpath(); - const options: cp.ExecSyncOptionsWithStringEncoding = { - cwd: cwd, - encoding: 'utf-8', - }; - - const args = [ - '--silent', - '--slave', - '--no-save', - '--no-restore', - '-e', - `rmarkdown::draft(file='${tempFile}', template='${template.id}', package='${template.package}', edit=FALSE)`, - ]; - - try { - const result = cp.spawnSync(rPath, args, options); - if (result.error) { - throw result.error; - } - - if (fs.existsSync(tempFile)) { - const text = fs.readFileSync(tempFile, 'utf-8'); - return text; - } else { - throw new Error('Failed to create draft.'); - } - } catch (e) { - void window.showErrorMessage((<{ message: string }>e).message); - } finally { - fs.rmdirSync(tempDir, { recursive: true }); - } - - return undefined; +async function makeDraft(file: string, template: TemplateItem, cwd: string): Promise { + const cmd = `cat(normalizePath(rmarkdown::draft(file='${file}', template='${template.id}', package='${template.package}', edit=FALSE)))`; + return await executeRCommand(cmd, cwd, (e: Error) => { + void window.showErrorMessage(e.message); + return ''; + }); } export async function newDraft(): Promise { @@ -137,8 +107,20 @@ export async function newDraft(): Promise { return; } - const text = await makeDraft(template, cwd); - if (text) { - void workspace.openTextDocument({ language: 'rmd', content: text }); + const uri = await window.showSaveDialog({ + defaultUri: Uri.file(join(cwd, 'draft.Rmd')), + filters: { + 'R Markdown': ['Rmd', 'rmd'] + }, + saveLabel: 'Create Draft', + title: 'R Markdown: New Draft' + }); + + if (uri) { + const draftPath = await makeDraft(uri.fsPath, template, cwd); + if (draftPath) { + await workspace.openTextDocument(draftPath) + .then(document => window.showTextDocument(document)); + } } } diff --git a/src/util.ts b/src/util.ts index edc954ca7..4c8c947d0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -286,7 +286,7 @@ export async function doWithProgress(cb: (token?: vscode.CancellationToken, p export async function getCranUrl(path: string = '', cwd?: string): Promise { const defaultCranUrl = 'https://cran.r-project.org/'; // get cran URL from R. Returns empty string if option is not set. - const baseUrl = await executeRCommand('cat(getOption(\'repos\')[\'CRAN\'])', undefined, cwd); + const baseUrl = await executeRCommand('cat(getOption(\'repos\')[\'CRAN\'])', cwd); let url: string; try { url = new URL(path, baseUrl).toString(); @@ -301,12 +301,12 @@ export async function getCranUrl(path: string = '', cwd?: string): Promise { +export async function executeRCommand(rCommand: string, cwd?: string, fallback?: string | ((e: Error) => string)): Promise { const lim = '---vsc---'; const re = new RegExp(`${lim}(.*)${lim}`, 'ms'); @@ -334,6 +334,9 @@ export async function executeRCommand(rCommand: string, fallBack?: string, cwd?: if (result.error) { throw result.error; } + if (result.status !== 0) { + throw new Error(result.stderr); + } const match = re.exec(result.stdout); if (match.length === 2) { ret = match[1]; @@ -341,8 +344,8 @@ export async function executeRCommand(rCommand: string, fallBack?: string, cwd?: throw new Error('Could not parse R output.'); } } catch (e) { - if (fallBack) { - ret = fallBack; + if (fallback) { + ret = (typeof fallback === 'function' ? fallback(e) : fallback); } else { console.warn(e); } @@ -465,7 +468,7 @@ export function exec(command: string, args?: ReadonlyArray, options?: cp */ export async function isRPkgIntalled(name: string, cwd: string, promptToInstall: boolean = false, installMsg?: string, postInstallMsg?: string): Promise { const cmd = `cat(requireNamespace('${name}', quietly=TRUE))`; - const rOut = await executeRCommand(cmd, 'FALSE', cwd); + const rOut = await executeRCommand(cmd, cwd, 'FALSE'); const isInstalled = rOut === 'TRUE'; if (promptToInstall && !isInstalled) { if (installMsg === undefined) { From e82989bbeba4c700691571b01a78f697a4618d36 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Wed, 9 Feb 2022 14:26:16 +0000 Subject: [PATCH 05/14] Fix file path --- src/rmarkdown/draft.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 8d287fe7b..9efce8658 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -8,7 +8,7 @@ import { QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; import { extensionContext } from '../extension'; -import { executeRCommand, getCurrentWorkspaceFolder, getRpath } from '../util'; +import { executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral } from '../util'; import * as cp from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; @@ -93,7 +93,8 @@ async function launchTemplatePicker(cwd: string): Promise { } async function makeDraft(file: string, template: TemplateItem, cwd: string): Promise { - const cmd = `cat(normalizePath(rmarkdown::draft(file='${file}', template='${template.id}', package='${template.package}', edit=FALSE)))`; + const fileString = ToRStringLiteral(file, ''); + const cmd = `cat(normalizePath(rmarkdown::draft(file='${fileString}', template='${template.id}', package='${template.package}', edit=FALSE)))`; return await executeRCommand(cmd, cwd, (e: Error) => { void window.showErrorMessage(e.message); return ''; From d310d5a925f260f3f3367ef06a03f72721f112ff Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Wed, 9 Feb 2022 22:50:10 +0800 Subject: [PATCH 06/14] Remove unused eslint-disable --- src/rmarkdown/draft.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 9efce8658..6b75eadfe 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -1,9 +1,6 @@ -/* eslint-disable @typescript-eslint/restrict-plus-operands */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; From 442b8251d11d70a7a92f77617423f09eca76415f Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 10 Feb 2022 12:52:22 +0800 Subject: [PATCH 07/14] Disable eslint no-explicit-any --- src/rmarkdown/draft.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 6b75eadfe..1b0d19541 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ From 8e70e324317c8a545b0a7cc642b59db6317120f1 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 10 Feb 2022 13:22:14 +0800 Subject: [PATCH 08/14] Handle create_dir --- src/rmarkdown/draft.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 1b0d19541..ff3b1ebae 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -14,9 +14,16 @@ import * as os from 'os'; import { readJSON } from 'fs-extra'; import { join } from 'path'; -interface TemplateItem extends QuickPickItem { +interface TemplateInfo { id: string; package: string; + name: string; + description: string; + create_dir: boolean; +} + +interface TemplateItem extends QuickPickItem { + info: TemplateInfo; } async function getTemplateItems(cwd: string): Promise { @@ -47,7 +54,7 @@ async function getTemplateItems(cwd: string): Promise { throw result.error; } - const templates: any[] = await readJSON(tempFile).then( + const templates: TemplateInfo[] = await readJSON(tempFile).then( (result) => result, () => { throw ('Failed to load templates from installed packages.'); @@ -61,8 +68,7 @@ async function getTemplateItems(cwd: string): Promise { label: x.name, detail: x.description, picked: false, - package: x.package, - id: x.id, + info: x }; }); @@ -92,7 +98,7 @@ async function launchTemplatePicker(cwd: string): Promise { async function makeDraft(file: string, template: TemplateItem, cwd: string): Promise { const fileString = ToRStringLiteral(file, ''); - const cmd = `cat(normalizePath(rmarkdown::draft(file='${fileString}', template='${template.id}', package='${template.package}', edit=FALSE)))`; + const cmd = `cat(normalizePath(rmarkdown::draft(file='${fileString}', template='${template.info.id}', package='${template.info.package}', edit=FALSE)))`; return await executeRCommand(cmd, cwd, (e: Error) => { void window.showErrorMessage(e.message); return ''; @@ -107,11 +113,11 @@ export async function newDraft(): Promise { } const uri = await window.showSaveDialog({ - defaultUri: Uri.file(join(cwd, 'draft.Rmd')), + defaultUri: Uri.file(join(cwd, template.info.create_dir ? 'draft' : 'draft.Rmd')), filters: { 'R Markdown': ['Rmd', 'rmd'] }, - saveLabel: 'Create Draft', + saveLabel: template.info.create_dir ? 'Create Folder' : 'Save', title: 'R Markdown: New Draft' }); From fa47d9e27a6050ecf4a11ddd7202284610e7b3e0 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Thu, 10 Feb 2022 23:45:53 +0800 Subject: [PATCH 09/14] Create untitled document if not create_dir --- src/rmarkdown/draft.ts | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index ff3b1ebae..aa16eb1c9 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -112,20 +112,32 @@ export async function newDraft(): Promise { return; } - const uri = await window.showSaveDialog({ - defaultUri: Uri.file(join(cwd, template.info.create_dir ? 'draft' : 'draft.Rmd')), - filters: { - 'R Markdown': ['Rmd', 'rmd'] - }, - saveLabel: template.info.create_dir ? 'Create Folder' : 'Save', - title: 'R Markdown: New Draft' - }); + if (template.info.create_dir) { + const uri = await window.showSaveDialog({ + defaultUri: Uri.file(join(cwd, 'draft')), + filters: { + 'R Markdown': ['Rmd', 'rmd'] + }, + saveLabel: 'Create Folder', + title: 'R Markdown: New Draft' + }); - if (uri) { - const draftPath = await makeDraft(uri.fsPath, template, cwd); + if (uri) { + const draftPath = await makeDraft(uri.fsPath, template, cwd); + if (draftPath) { + await workspace.openTextDocument(draftPath) + .then(document => window.showTextDocument(document)); + } + } + } else { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-')); + const tempFile = path.join(tempDir, 'draft.Rmd'); + const draftPath = await makeDraft(tempFile, template, cwd); if (draftPath) { - await workspace.openTextDocument(draftPath) + const text = fs.readFileSync(draftPath, 'utf8'); + await workspace.openTextDocument({ language: 'rmd', content: text }) .then(document => window.showTextDocument(document)); } + fs.rmdirSync(tempDir, { recursive: true }); } } From b7cdb6f92a383004789c756480ecad1f7e11f705 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 11 Feb 2022 00:58:28 +0800 Subject: [PATCH 10/14] Use spawn --- R/rmarkdown/templates.R | 6 ++-- src/rmarkdown/draft.ts | 77 ++++++++++++++++++++++------------------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/R/rmarkdown/templates.R b/R/rmarkdown/templates.R index 73114f7f0..dec8b97d0 100644 --- a/R/rmarkdown/templates.R +++ b/R/rmarkdown/templates.R @@ -20,6 +20,6 @@ template_dirs <- lapply(pkgs, function(pkg) { }) template_list <- unname(as.list(templates)) -file <- Sys.getenv("VSCR_FILE") - -jsonlite::write_json(template_list, file, auto_unbox = TRUE) +lim <- Sys.getenv("VSCR_LIM") +json <- jsonlite::toJSON(template_list, auto_unbox = TRUE) +cat(lim, json, lim, sep = "\n", file = stdout()) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index aa16eb1c9..2bd3d26cc 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -6,12 +6,11 @@ import { QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; import { extensionContext } from '../extension'; -import { executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral } from '../util'; +import { exec, executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral } from '../util'; import * as cp from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; -import { readJSON } from 'fs-extra'; import { join } from 'path'; interface TemplateInfo { @@ -27,57 +26,65 @@ interface TemplateItem extends QuickPickItem { } async function getTemplateItems(cwd: string): Promise { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-')); - const tempFile = path.join(tempDir, 'templates.json'); + const lim = '---vsc---'; const rPath = await getRpath(); const options: cp.ExecSyncOptionsWithStringEncoding = { cwd: cwd, encoding: 'utf-8', env: { ...process.env, - VSCR_FILE: tempFile + VSCR_LIM: lim } }; + const rScriptFile = extensionContext.asAbsolutePath('R/rmarkdown/templates.R'); const args = [ '--silent', '--slave', '--no-save', '--no-restore', '-f', - extensionContext.asAbsolutePath('R/rmarkdown/templates.R') + rScriptFile ]; - try { - const result = cp.spawnSync(rPath, args, options); - if (result.error) { - throw result.error; + return new Promise((resolve) => { + try { + let str = ''; + const childProcess = exec(rPath, args, options); + childProcess.stdout?.on('data', (chunk: Buffer) => { + str += chunk.toString(); + }); + childProcess.on('exit', (code, signal) => { + let items: TemplateItem[] = []; + if (code === 0) { + const re = new RegExp(`${lim}(.*)${lim}`, 'ms'); + const match = re.exec(str); + if (match.length === 2) { + const json = match[1]; + const templates = JSON.parse(json) || []; + items = templates.map((x) => { + return { + alwaysShow: false, + description: `{${x.package}}`, + label: x.name, + detail: x.description, + picked: false, + info: x + }; + }); + } else { + console.log('Could not parse R output.'); + } + } else { + console.log(`R process exited with code ${code} from signal ${signal}`); + } + resolve(items); + }); + } catch (e) { + void window.showErrorMessage((<{ message: string }>e).message); + resolve([]); } - - const templates: TemplateInfo[] = await readJSON(tempFile).then( - (result) => result, - () => { - throw ('Failed to load templates from installed packages.'); - } - ); - - const items = templates.map((x) => { - return { - alwaysShow: false, - description: `{${x.package}}`, - label: x.name, - detail: x.description, - picked: false, - info: x - }; - }); - - return items; - } catch (e) { - void window.showErrorMessage((<{ message: string }>e).message); - } finally { - fs.rmdirSync(tempDir, { recursive: true }); - } + }); } async function launchTemplatePicker(cwd: string): Promise { From c0ba218487479544f311a2a016fa70856f281dc5 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 11 Feb 2022 18:54:37 +0800 Subject: [PATCH 11/14] Update getTemplateItems --- src/rmarkdown/draft.ts | 71 ++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 2bd3d26cc..6ee2333d4 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -6,7 +6,7 @@ import { QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; import { extensionContext } from '../extension'; -import { exec, executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral } from '../util'; +import { spawn, executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral, spawnAsync } from '../util'; import * as cp from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; @@ -28,9 +28,8 @@ interface TemplateItem extends QuickPickItem { async function getTemplateItems(cwd: string): Promise { const lim = '---vsc---'; const rPath = await getRpath(); - const options: cp.ExecSyncOptionsWithStringEncoding = { + const options: cp.CommonOptions = { cwd: cwd, - encoding: 'utf-8', env: { ...process.env, VSCR_LIM: lim @@ -47,44 +46,36 @@ async function getTemplateItems(cwd: string): Promise { rScriptFile ]; - return new Promise((resolve) => { - try { - let str = ''; - const childProcess = exec(rPath, args, options); - childProcess.stdout?.on('data', (chunk: Buffer) => { - str += chunk.toString(); - }); - childProcess.on('exit', (code, signal) => { - let items: TemplateItem[] = []; - if (code === 0) { - const re = new RegExp(`${lim}(.*)${lim}`, 'ms'); - const match = re.exec(str); - if (match.length === 2) { - const json = match[1]; - const templates = JSON.parse(json) || []; - items = templates.map((x) => { - return { - alwaysShow: false, - description: `{${x.package}}`, - label: x.name, - detail: x.description, - picked: false, - info: x - }; - }); - } else { - console.log('Could not parse R output.'); - } - } else { - console.log(`R process exited with code ${code} from signal ${signal}`); - } - resolve(items); - }); - } catch (e) { - void window.showErrorMessage((<{ message: string }>e).message); - resolve([]); + let items: TemplateItem[] = []; + + try { + const result = await spawnAsync(rPath, args, options); + if (result.status !== 0) { + throw result.error || new Error(result.stderr); } - }); + const re = new RegExp(`${lim}(.*)${lim}`, 'ms'); + const match = re.exec(result.stdout); + if (match.length !== 2) { + throw new Error('Could not parse R output.'); + } + const json = match[1]; + const templates = JSON.parse(json) || []; + items = templates.map((x) => { + return { + alwaysShow: false, + description: `{${x.package}}`, + label: x.name, + detail: x.description, + picked: false, + info: x + }; + }); + } catch (e) { + console.log(e); + void window.showErrorMessage((<{ message: string }>e).message); + } + + return items; } async function launchTemplatePicker(cwd: string): Promise { From 4b0463cac46a65d297053ffbb3d0dff889d5e0ae Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 11 Feb 2022 18:57:28 +0800 Subject: [PATCH 12/14] Format file --- src/rmarkdown/draft.ts | 207 ++++++++++++++++++++--------------------- 1 file changed, 100 insertions(+), 107 deletions(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 6ee2333d4..5b30e4943 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -1,141 +1,134 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ - import { QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; import { extensionContext } from '../extension'; -import { spawn, executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral, spawnAsync } from '../util'; +import { executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral, spawnAsync } from '../util'; import * as cp from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; -import { join } from 'path'; interface TemplateInfo { - id: string; - package: string; - name: string; - description: string; - create_dir: boolean; + id: string; + package: string; + name: string; + description: string; + create_dir: boolean; } interface TemplateItem extends QuickPickItem { - info: TemplateInfo; + info: TemplateInfo; } async function getTemplateItems(cwd: string): Promise { - const lim = '---vsc---'; - const rPath = await getRpath(); - const options: cp.CommonOptions = { - cwd: cwd, - env: { - ...process.env, - VSCR_LIM: lim - } - }; + const lim = '---vsc---'; + const rPath = await getRpath(); + const options: cp.CommonOptions = { + cwd: cwd, + env: { + ...process.env, + VSCR_LIM: lim + } + }; - const rScriptFile = extensionContext.asAbsolutePath('R/rmarkdown/templates.R'); - const args = [ - '--silent', - '--slave', - '--no-save', - '--no-restore', - '-f', - rScriptFile - ]; + const rScriptFile = extensionContext.asAbsolutePath('R/rmarkdown/templates.R'); + const args = [ + '--silent', + '--slave', + '--no-save', + '--no-restore', + '-f', + rScriptFile + ]; - let items: TemplateItem[] = []; + let items: TemplateItem[] = []; - try { - const result = await spawnAsync(rPath, args, options); - if (result.status !== 0) { - throw result.error || new Error(result.stderr); + try { + const result = await spawnAsync(rPath, args, options); + if (result.status !== 0) { + throw result.error || new Error(result.stderr); + } + const re = new RegExp(`${lim}(.*)${lim}`, 'ms'); + const match = re.exec(result.stdout); + if (match.length !== 2) { + throw new Error('Could not parse R output.'); + } + const json = match[1]; + const templates = JSON.parse(json) || []; + items = templates.map((x) => { + return { + alwaysShow: false, + description: `{${x.package}}`, + label: x.name, + detail: x.description, + picked: false, + info: x + }; + }); + } catch (e) { + console.log(e); + void window.showErrorMessage((<{ message: string }>e).message); } - const re = new RegExp(`${lim}(.*)${lim}`, 'ms'); - const match = re.exec(result.stdout); - if (match.length !== 2) { - throw new Error('Could not parse R output.'); - } - const json = match[1]; - const templates = JSON.parse(json) || []; - items = templates.map((x) => { - return { - alwaysShow: false, - description: `{${x.package}}`, - label: x.name, - detail: x.description, - picked: false, - info: x - }; - }); - } catch (e) { - console.log(e); - void window.showErrorMessage((<{ message: string }>e).message); - } - return items; + return items; } async function launchTemplatePicker(cwd: string): Promise { - const options: QuickPickOptions = { - matchOnDescription: true, - matchOnDetail: true, - canPickMany: false, - ignoreFocusOut: false, - placeHolder: '', - onDidSelectItem: undefined - }; + const options: QuickPickOptions = { + matchOnDescription: true, + matchOnDetail: true, + canPickMany: false, + ignoreFocusOut: false, + placeHolder: '', + onDidSelectItem: undefined + }; - const items = await getTemplateItems(cwd); + const items = await getTemplateItems(cwd); - const selection: TemplateItem = await window.showQuickPick(items, options); - return selection; + const selection: TemplateItem = await window.showQuickPick(items, options); + return selection; } async function makeDraft(file: string, template: TemplateItem, cwd: string): Promise { - const fileString = ToRStringLiteral(file, ''); - const cmd = `cat(normalizePath(rmarkdown::draft(file='${fileString}', template='${template.info.id}', package='${template.info.package}', edit=FALSE)))`; - return await executeRCommand(cmd, cwd, (e: Error) => { - void window.showErrorMessage(e.message); - return ''; - }); + const fileString = ToRStringLiteral(file, ''); + const cmd = `cat(normalizePath(rmarkdown::draft(file='${fileString}', template='${template.info.id}', package='${template.info.package}', edit=FALSE)))`; + return await executeRCommand(cmd, cwd, (e: Error) => { + void window.showErrorMessage(e.message); + return ''; + }); } export async function newDraft(): Promise { - const cwd = getCurrentWorkspaceFolder()?.uri.fsPath ?? os.homedir(); - const template = await launchTemplatePicker(cwd); - if (!template) { - return; - } + const cwd = getCurrentWorkspaceFolder()?.uri.fsPath ?? os.homedir(); + const template = await launchTemplatePicker(cwd); + if (!template) { + return; + } - if (template.info.create_dir) { - const uri = await window.showSaveDialog({ - defaultUri: Uri.file(join(cwd, 'draft')), - filters: { - 'R Markdown': ['Rmd', 'rmd'] - }, - saveLabel: 'Create Folder', - title: 'R Markdown: New Draft' - }); + if (template.info.create_dir) { + const uri = await window.showSaveDialog({ + defaultUri: Uri.file(path.join(cwd, 'draft')), + filters: { + 'R Markdown': ['Rmd', 'rmd'] + }, + saveLabel: 'Create Folder', + title: 'R Markdown: New Draft' + }); - if (uri) { - const draftPath = await makeDraft(uri.fsPath, template, cwd); - if (draftPath) { - await workspace.openTextDocument(draftPath) - .then(document => window.showTextDocument(document)); - } - } - } else { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-')); - const tempFile = path.join(tempDir, 'draft.Rmd'); - const draftPath = await makeDraft(tempFile, template, cwd); - if (draftPath) { - const text = fs.readFileSync(draftPath, 'utf8'); - await workspace.openTextDocument({ language: 'rmd', content: text }) - .then(document => window.showTextDocument(document)); + if (uri) { + const draftPath = await makeDraft(uri.fsPath, template, cwd); + if (draftPath) { + await workspace.openTextDocument(draftPath) + .then(document => window.showTextDocument(document)); + } + } + } else { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-')); + const tempFile = path.join(tempDir, 'draft.Rmd'); + const draftPath = await makeDraft(tempFile, template, cwd); + if (draftPath) { + const text = fs.readFileSync(draftPath, 'utf8'); + await workspace.openTextDocument({ language: 'rmd', content: text }) + .then(document => window.showTextDocument(document)); + } + fs.rmdirSync(tempDir, { recursive: true }); } - fs.rmdirSync(tempDir, { recursive: true }); - } } From a5f8f98d129e987250155032a3064ae14845bb6a Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 11 Feb 2022 21:35:45 +0800 Subject: [PATCH 13/14] Update getTemplateItems --- src/rmarkdown/draft.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 5b30e4943..ba1138217 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -39,8 +39,6 @@ async function getTemplateItems(cwd: string): Promise { rScriptFile ]; - let items: TemplateItem[] = []; - try { const result = await spawnAsync(rPath, args, options); if (result.status !== 0) { @@ -53,22 +51,21 @@ async function getTemplateItems(cwd: string): Promise { } const json = match[1]; const templates = JSON.parse(json) || []; - items = templates.map((x) => { + const items = templates.map((x) => { return { alwaysShow: false, description: `{${x.package}}`, - label: x.name, + label: x.name + (x.create_dir ? ' $(new-folder)' : ''), detail: x.description, picked: false, info: x }; }); + return items; } catch (e) { console.log(e); void window.showErrorMessage((<{ message: string }>e).message); } - - return items; } async function launchTemplatePicker(cwd: string): Promise { From 32b7f68023ecacb5f41a2604aa4581bd1a261492 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Fri, 11 Feb 2022 23:17:48 +0800 Subject: [PATCH 14/14] Update confirm replacing folder --- src/rmarkdown/draft.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index ba1138217..4a716e27e 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -1,6 +1,6 @@ import { QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; import { extensionContext } from '../extension'; -import { executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral, spawnAsync } from '../util'; +import { executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral, spawnAsync, getConfirmation } from '../util'; import * as cp from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; @@ -101,16 +101,31 @@ export async function newDraft(): Promise { } if (template.info.create_dir) { + let defaultPath = path.join(cwd, 'draft'); + let i = 1; + while (fs.existsSync(defaultPath)) { + defaultPath = path.join(cwd, `draft_${++i}`); + } const uri = await window.showSaveDialog({ - defaultUri: Uri.file(path.join(cwd, 'draft')), + defaultUri: Uri.file(defaultPath), filters: { - 'R Markdown': ['Rmd', 'rmd'] + 'Folder': [''] }, saveLabel: 'Create Folder', title: 'R Markdown: New Draft' }); if (uri) { + const parsedPath = path.parse(uri.fsPath); + const dir = path.join(parsedPath.dir, parsedPath.name); + if (fs.existsSync(dir)) { + if (await getConfirmation(`Folder already exists. Are you sure to replace the folder?`)) { + fs.rmdirSync(dir, { recursive: true }); + } else { + return; + } + } + const draftPath = await makeDraft(uri.fsPath, template, cwd); if (draftPath) { await workspace.openTextDocument(draftPath)