Skip to content

Commit e2b28f1

Browse files
authored
Fix code execution, R Markdown, and editor request bugs (#1874)
* fix: preserve R code selections, strings, and process output * fix: settle R Markdown jobs and preserve quoted paths * fix: preserve existing files when generation is cancelled * fix: await RStudio editor edits and correct line coordinates * test: respect workspace URI normalization on Windows * fix: restore lint configuration when replacement generation fails * fix: reject RStudio requests when editor changes cannot apply
1 parent 18bdedf commit e2b28f1

19 files changed

Lines changed: 645 additions & 177 deletions

‎src/cppProperties.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ export async function generateCppProperties(): Promise<void> {
1919
'"c_cpp_properties.json" file already exists. Do you want to overwrite?',
2020
'Yes', 'No'
2121
);
22-
if (overwrite === 'No') {
22+
if (overwrite !== 'Yes') {
2323
return;
2424
}
25-
fs.unlinkSync(outFilePath);
2625
}
2726
return generateCppPropertiesProc(currentWorkspaceFolder);
2827
}

‎src/lineCache.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export function cleanLine(text: string): string {
5151
let withinQuotes = null;
5252
for (let i = 0; i < text.length; i++) {
5353
const c = text[i];
54+
if (withinQuotes && c === '\\') {
55+
cleaned += c;
56+
if (i + 1 < text.length) { cleaned += text[++i]; }
57+
continue;
58+
}
5459
if (isQuote(c)) {
5560
if (withinQuotes === null) {
5661
withinQuotes = c;

‎src/lintrConfig.ts‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use strict';
22

3-
import { existsSync, unlinkSync } from 'fs';
3+
import { existsSync, mkdtempSync, renameSync, rmSync } from 'fs';
44
import { join } from 'path';
55
import { window } from 'vscode';
6-
import { executeRCommand, getCurrentWorkspaceFolder } from './util';
6+
import { catchAsError, executeRCommand, getCurrentWorkspaceFolder } from './util';
77

88
export async function createLintrConfig(): Promise<string | undefined> {
99
const currentWorkspaceFolder = getCurrentWorkspaceFolder()?.uri.fsPath;
@@ -17,13 +17,34 @@ export async function createLintrConfig(): Promise<string | undefined> {
1717
'".lintr" file already exists. Do you want to overwrite?',
1818
'Yes', 'No'
1919
);
20-
if (overwrite === 'No') {
20+
if (overwrite !== 'Yes') {
2121
return;
2222
}
23-
unlinkSync(lintrFilePath);
2423
}
25-
return await executeRCommand(`lintr::use_lintr()`, currentWorkspaceFolder, (e: Error) => {
26-
void window.showErrorMessage(e.message);
24+
25+
let backupDirectory: string | undefined;
26+
let generated = false;
27+
try {
28+
if (existsSync(lintrFilePath)) {
29+
// lintr refuses to overwrite an existing file. Keep the original on
30+
// disk until it has successfully generated the replacement.
31+
backupDirectory = mkdtempSync(join(currentWorkspaceFolder, '.vscode-r-lintr-'));
32+
renameSync(lintrFilePath, join(backupDirectory, '.lintr'));
33+
}
34+
const output = await executeRCommand('lintr::use_lintr()', currentWorkspaceFolder, (error: Error) => { throw error; });
35+
if (output !== undefined) {
36+
if (!existsSync(lintrFilePath)) { throw new Error('R did not create a .lintr configuration.'); }
37+
generated = true;
38+
}
39+
return output;
40+
} catch (error) {
41+
void window.showErrorMessage(catchAsError(error).message);
2742
return '';
28-
});
43+
} finally {
44+
if (backupDirectory) {
45+
const backupPath = join(backupDirectory, '.lintr');
46+
if (!generated && existsSync(backupPath)) { renameSync(backupPath, lintrFilePath); }
47+
rmSync(backupDirectory, { recursive: true, force: true });
48+
}
49+
}
2950
}

‎src/rGitignore.ts‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
import { writeFile } from 'node:fs';
3+
import { writeFile } from 'node:fs/promises';
44
import { existsSync, readFileSync } from 'fs';
55
import { join } from 'path';
66
import { window } from 'vscode';
@@ -23,17 +23,13 @@ export async function createGitignore(): Promise<void> {
2323
'".gitignore" file already exists. Do you want to overwrite?',
2424
'Yes', 'No'
2525
);
26-
if (overwrite === 'No') {
26+
if (overwrite !== 'Yes') {
2727
return;
2828
}
2929
}
30-
writeFile(ignorePath, ignoreFileContent, (err) => {
31-
try {
32-
if (err) {
33-
void window.showErrorMessage(err.name);
34-
}
35-
} catch (e) {
36-
void window.showErrorMessage(catchAsError(e).message);
37-
}
38-
});
30+
try {
31+
await writeFile(ignorePath, ignoreFileContent);
32+
} catch (e) {
33+
void window.showErrorMessage(catchAsError(e).message);
34+
}
3935
}

‎src/rmarkdown/chunks.ts‎

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,27 @@ export function getChunks(document: vscode.TextDocument): RMarkdownChunk[] {
8585
let line = 0;
8686
let chunkId = 0; // One-based index
8787
let chunkStartLine: number | undefined = undefined;
88-
let chunkEndLine: number | undefined = undefined;
89-
let codeEndLine: number | undefined = undefined;
9088
let chunkLanguage: string | undefined = undefined;
9189
let chunkOptions: string | undefined = undefined;
9290
let chunkEval: boolean | undefined = undefined;
9391
const isRDoc = isRDocument(document);
9492

93+
const finishChunk = (endLine: number, codeEndLine: number) => {
94+
const startLine = chunkStartLine!;
95+
const codeStart = codeEndLine > startLine
96+
? new vscode.Position(startLine + 1, 0)
97+
: new vscode.Position(startLine, lines[startLine].length);
98+
const codeEnd = codeEndLine > startLine
99+
? new vscode.Position(codeEndLine, lines[codeEndLine].length) : codeStart;
100+
chunks.push({
101+
id: chunkId, startLine, endLine, language: chunkLanguage,
102+
options: chunkOptions, eval: chunkEval,
103+
chunkRange: new vscode.Range(startLine, 0, endLine, lines[endLine].length),
104+
codeRange: new vscode.Range(codeStart, codeEnd)
105+
});
106+
chunkStartLine = undefined;
107+
};
108+
95109
while (line < lines.length) {
96110
if (chunkStartLine === undefined) {
97111
if (isChunkStartLine(lines[line], isRDoc)) {
@@ -102,44 +116,18 @@ export function getChunks(document: vscode.TextDocument): RMarkdownChunk[] {
102116
chunkEval = getChunkEval(chunkOptions);
103117
}
104118
} else {
105-
// Second condition is for the last chunk in an .R file
106-
const isRDocAndFinalLine = (isRDoc && line === lines.length - 1);
107-
if (isChunkEndLine(lines[line], isRDoc) || isRDocAndFinalLine) {
108-
chunkEndLine = line;
109-
codeEndLine = line - 1;
110-
111-
// isChunkEndLine looks for `# %%` in `.R` files, so if found, then need to go back one line to mark end of code chunk.
112-
if (isRDoc && !isRDocAndFinalLine) {
113-
chunkEndLine = chunkEndLine - 1;
114-
codeEndLine = chunkEndLine;
115-
line = line - 1;
116-
}
117-
118-
const chunkRange = new vscode.Range(
119-
new vscode.Position(chunkStartLine, 0),
120-
new vscode.Position(line, lines[line].length)
121-
);
122-
const codeRange = new vscode.Range(
123-
new vscode.Position(chunkStartLine + 1, 0),
124-
new vscode.Position(codeEndLine, lines[codeEndLine].length)
125-
);
126-
127-
chunks.push({
128-
id: chunkId, // One-based index
129-
startLine: chunkStartLine,
130-
endLine: chunkEndLine,
131-
language: chunkLanguage,
132-
options: chunkOptions,
133-
eval: chunkEval,
134-
chunkRange: chunkRange,
135-
codeRange: codeRange
136-
});
137-
138-
chunkStartLine = undefined;
119+
if (isChunkEndLine(lines[line], isRDoc)) {
120+
finishChunk(isRDoc ? line - 1 : line, line - 1);
121+
// An R marker ends the previous chunk and starts the next one,
122+
// including when the marker is the document's final line.
123+
if (isRDoc) { line--; }
139124
}
140125
}
141126
line++;
142127
}
128+
if (isRDoc && chunkStartLine !== undefined) {
129+
finishChunk(lines.length - 1, lines.length - 1);
130+
}
143131
return chunks;
144132
}
145133

‎src/rmarkdown/draft.ts‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ async function launchTemplatePicker(cwd: string): Promise<TemplateItem | undefin
101101
}
102102

103103
async function makeDraft(file: string, template: TemplateItem, cwd: string): Promise<string | undefined> {
104-
const fileString = ToRStringLiteral(file, '');
105-
const cmd = `cat(normalizePath(rmarkdown::draft(file='${fileString}', template='${template.info.id}', package='${template.info.package}', edit=FALSE)))`;
104+
const cmd = `cat(normalizePath(rmarkdown::draft(file=${ToRStringLiteral(file, '"')}, template=${ToRStringLiteral(template.info.id, '"')}, package=${ToRStringLiteral(template.info.package, '"')}, edit=FALSE)))`;
106105
return await executeRCommand(cmd, cwd, (e: Error) => {
107106
void window.showErrorMessage(e.message);
108107
return '';
@@ -136,7 +135,7 @@ export async function newDraft(): Promise<void> {
136135
const dir = path.join(parsedPath.dir, parsedPath.name);
137136
if (fs.existsSync(dir)) {
138137
if (await getConfirmation(`Folder already exists. Are you sure you want to replace the folder?`)) {
139-
fs.rmdirSync(dir, { recursive: true });
138+
fs.rmSync(dir, { recursive: true, force: true });
140139
} else {
141140
return;
142141
}
@@ -151,12 +150,15 @@ export async function newDraft(): Promise<void> {
151150
} else {
152151
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-R-'));
153152
const tempFile = path.join(tempDir, 'draft.Rmd');
154-
const draftPath = await makeDraft(tempFile, template, cwd);
155-
if (draftPath) {
156-
const text = fs.readFileSync(draftPath, 'utf8');
157-
await workspace.openTextDocument({ language: 'rmd', content: text })
158-
.then(document => window.showTextDocument(document));
153+
try {
154+
const draftPath = await makeDraft(tempFile, template, cwd);
155+
if (draftPath) {
156+
const text = fs.readFileSync(draftPath, 'utf8');
157+
await workspace.openTextDocument({ language: 'rmd', content: text })
158+
.then(document => window.showTextDocument(document));
159+
}
160+
} finally {
161+
fs.rmSync(tempDir, { recursive: true, force: true });
159162
}
160-
fs.rmdirSync(tempDir, { recursive: true });
161163
}
162164
}

‎src/rmarkdown/knit.ts‎

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,10 @@ export class RMarkdownKnitManager extends RMarkdownManager {
116116
let paramObj: IYamlFrontmatter = {};
117117
if (yamlText) {
118118
try {
119-
paramObj = yaml.load(
120-
yamlText
121-
) as IYamlFrontmatter;
119+
const parsed: unknown = yaml.load(yamlText);
120+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
121+
paramObj = parsed as IYamlFrontmatter;
122+
}
122123
} catch (e) {
123124
console.error(`Could not parse YAML frontmatter for "${docPath}". Error: ${String(e)}`);
124125
}
@@ -136,26 +137,21 @@ export class RMarkdownKnitManager extends RMarkdownManager {
136137

137138
// precedence:
138139
// knit > site > configuration
139-
if (yamlParams?.['knit']) {
140+
const formatArg = outputFormat ? `, output_format = ${util.ToRStringLiteral(outputFormat, '"')}` : '';
141+
if (typeof yamlParams['knit'] === 'string' && yamlParams['knit']) {
140142
const knitParam = yamlParams['knit'].trim();
141-
knitCommand = outputFormat ?
142-
`${knitParam}(${docPath}, output_format = '${outputFormat}')` :
143-
`${knitParam}(${docPath})`;
143+
knitCommand = `(${knitParam})(${docPath}${formatArg})`;
144144
} else if (!this.isREADME(docPath) && yamlParams?.['site']) {
145-
knitCommand = outputFormat ?
146-
`rmarkdown::render_site(${docPath}, output_format = '${outputFormat}')` :
147-
`rmarkdown::render_site(${docPath})`;
145+
knitCommand = `rmarkdown::render_site(${docPath}${formatArg})`;
148146
} else {
149147
const cmd = util.config().get<string>('rmarkdown.knit.command');
150148
if (!cmd) {
151149
return;
152150
}
153-
knitCommand = outputFormat ?
154-
`${cmd}(${docPath}, output_format = '${outputFormat}')` :
155-
`${cmd}(${docPath})`;
151+
knitCommand = `${cmd}(${docPath}${formatArg})`;
156152
}
157153

158-
return knitCommand.replace(/['"]/g, '\'');
154+
return knitCommand;
159155
}
160156

161157
// check if the workspace of the document is a R Markdown site.
@@ -166,8 +162,8 @@ export class RMarkdownKnitManager extends RMarkdownManager {
166162
if (!wad) {
167163
return;
168164
}
169-
const rootFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath ?? path.dirname(wad);
170-
const indexFile = (await vscode.workspace.findFiles(new vscode.RelativePattern(rootFolder, 'index.{Rmd,rmd, md}'), null, 1))?.[0];
165+
const rootFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(wad))?.uri.fsPath ?? path.dirname(wad);
166+
const indexFile = (await vscode.workspace.findFiles(new vscode.RelativePattern(rootFolder, 'index.{Rmd,rmd,md}'), null, 1))?.[0];
171167
const siteRoot = path.join(path.dirname(wad), '_site.yml');
172168

173169
// 'Simple' R Markdown websites require all docs to be in the root folder
@@ -275,19 +271,22 @@ export class RMarkdownKnitManager extends RMarkdownManager {
275271
return;
276272
}
277273
this.busyUriStore.add(busyPath);
278-
await this.renderDocument(
279-
rDocumentPath,
280-
wad.uri.fsPath,
281-
path.basename(wad.uri.fsPath),
282-
this.getYamlFrontmatter(wad.uri.fsPath),
283-
outputFormat
284-
);
285-
this.busyUriStore.delete(busyPath);
274+
try {
275+
await this.renderDocument(
276+
rDocumentPath,
277+
wad.uri.fsPath,
278+
path.basename(wad.uri.fsPath),
279+
this.getYamlFrontmatter(wad.uri.fsPath),
280+
outputFormat
281+
);
282+
} finally {
283+
this.busyUriStore.delete(busyPath);
284+
}
286285
} else {
287286
if (outputFormat === undefined) {
288287
void runTextInTerm(`rmarkdown::render(${rDocumentPath})`);
289288
} else {
290-
void runTextInTerm(`rmarkdown::render(${rDocumentPath}, '${outputFormat}')`);
289+
void runTextInTerm(`rmarkdown::render(${rDocumentPath}, ${util.ToRStringLiteral(outputFormat, '"')})`);
291290
}
292291
}
293292
}

0 commit comments

Comments
 (0)