Skip to content

Commit d7eaa67

Browse files
authored
fix: highlight nested R Markdown chunks through the outer fence (#1947)
The scanner now stores the opening fence length and closes the chunk only when a later fence is at least that long. A shorter fence stays inside the outer chunk as content. A fence that meets the length still ends the chunk, and a following chunk is parsed on its own. Added cases cover a longer outer fence around a shorter example, a longer fence before the next chunk, and info strings, bare fences, and unclosed chunks. Fixes #1500 Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
1 parent ada9a6c commit d7eaa67

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

‎src/rmarkdown/chunks.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ function isChunkStartLine(text: string, isRDoc: boolean) {
1818
}
1919
}
2020

21-
function isChunkEndLine(text: string, isRDoc: boolean) {
21+
function fenceLength(text: string) {
22+
return text.match(/^\s*(`{3,})/)?.[1].length ?? 0;
23+
}
24+
25+
function isChunkEndLine(text: string, isRDoc: boolean, minBackticks = 3) {
2226
if (isRDoc) {
2327
const isSectionHeader = text.match(/^#+\s*.*[-#+=*]{4,}/g);
2428
return isRChunkLine(text) || isSectionHeader;
2529
} else {
26-
return !!text.match(/^\s*```+\s*$/g);
30+
const match = text.match(/^\s*(`{3,})\s*$/);
31+
return !!match && match[1].length >= minBackticks;
2732
}
2833
}
2934

@@ -97,6 +102,7 @@ export function getChunks(document: vscode.TextDocument): RMarkdownChunk[] {
97102
let line = 0;
98103
let chunkId = 0; // One-based index
99104
let chunkStartLine: number | undefined = undefined;
105+
let chunkFenceLength = 0;
100106
let chunkLanguage: string | undefined = undefined;
101107
let chunkOptions: string | undefined = undefined;
102108
let chunkEval: boolean | undefined = undefined;
@@ -130,12 +136,13 @@ export function getChunks(document: vscode.TextDocument): RMarkdownChunk[] {
130136
if (isChunkStartLine(lines[line], isRDoc)) {
131137
chunkId++;
132138
chunkStartLine = line;
139+
chunkFenceLength = isRDoc ? 0 : fenceLength(lines[line]);
133140
chunkLanguage = getChunkLanguage(lines[line], isRDoc);
134141
chunkOptions = getChunkOptions(lines[line], isRDoc);
135142
chunkEval = getChunkEval(chunkOptions);
136143
}
137144
} else {
138-
if (isChunkEndLine(lines[line], isRDoc)) {
145+
if (isChunkEndLine(lines[line], isRDoc, chunkFenceLength)) {
139146
finishChunk(isRDoc ? line - 1 : line, line - 1);
140147
// An R marker ends the previous chunk and starts the next one,
141148
// including when the marker is the document's final line.

‎src/test/suite/chunks.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,44 @@ suite('R code chunk boundaries', () => {
2424
assert.deepStrictEqual(await chunkCode('```{r}\nx <- 1\n```', 'rmd'), ['x <- 1']);
2525
});
2626

27+
test('keeps a longer outer fence open through a shorter inner example', async () => {
28+
const text = '````{verbatim}\n\n```{r}\nfoo <- 1\n```\n\n````';
29+
const document = await vscode.workspace.openTextDocument({
30+
language: 'rmd',
31+
content: text,
32+
});
33+
const chunks = getChunks(document);
34+
assert.strictEqual(chunks.length, 1);
35+
assert.strictEqual(chunks[0].language, 'verbatim');
36+
assert.strictEqual(chunks[0].startLine, 0);
37+
assert.strictEqual(chunks[0].endLine, 6);
38+
assert.deepStrictEqual(document.getText(chunks[0].codeRange).split('\n'), [
39+
'',
40+
'```{r}',
41+
'foo <- 1',
42+
'```',
43+
'',
44+
]);
45+
});
46+
47+
test('ends a chunk on a longer fence and resumes with the next real chunk', async () => {
48+
assert.deepStrictEqual(await chunkCode('```{r}\nx <- 1\n````', 'rmd'), ['x <- 1']);
49+
const nestedThenReal =
50+
'````{verbatim}\n\n```{r}\nfoo <- 1\n```\n\n````\n```{r}\nx <- 1\n```';
51+
assert.deepStrictEqual(await chunkCode(nestedThenReal, 'rmd'), [
52+
'\n```{r}\nfoo <- 1\n```\n',
53+
'x <- 1',
54+
]);
55+
});
56+
57+
test('ignores info strings, bare fences, and unclosed R Markdown chunks', async () => {
58+
assert.deepStrictEqual(await chunkCode('```{r}\nx <- 1\n``` label\n```', 'rmd'), [
59+
'x <- 1\n``` label',
60+
]);
61+
assert.deepStrictEqual(await chunkCode('```\ncode\n```', 'rmd'), []);
62+
assert.deepStrictEqual(await chunkCode('```{r}\nx <- 1', 'rmd'), []);
63+
});
64+
2765
test('section headers end an R chunk without dropping its preceding line', async () => {
2866
assert.deepStrictEqual(await chunkCode('# %%\nx <- 1\n# section ----'), ['x <- 1']);
2967
});

0 commit comments

Comments
 (0)