Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/rmarkdown/chunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ function isChunkStartLine(text: string, isRDoc: boolean) {
}
}

function isChunkEndLine(text: string, isRDoc: boolean) {
function fenceLength(text: string) {
return text.match(/^\s*(`{3,})/)?.[1].length ?? 0;
}

function isChunkEndLine(text: string, isRDoc: boolean, minBackticks = 3) {
if (isRDoc) {
const isSectionHeader = text.match(/^#+\s*.*[-#+=*]{4,}/g);
return isRChunkLine(text) || isSectionHeader;
} else {
return !!text.match(/^\s*```+\s*$/g);
const match = text.match(/^\s*(`{3,})\s*$/);
return !!match && match[1].length >= minBackticks;
}
}

Expand Down Expand Up @@ -97,6 +102,7 @@ export function getChunks(document: vscode.TextDocument): RMarkdownChunk[] {
let line = 0;
let chunkId = 0; // One-based index
let chunkStartLine: number | undefined = undefined;
let chunkFenceLength = 0;
let chunkLanguage: string | undefined = undefined;
let chunkOptions: string | undefined = undefined;
let chunkEval: boolean | undefined = undefined;
Expand Down Expand Up @@ -130,12 +136,13 @@ export function getChunks(document: vscode.TextDocument): RMarkdownChunk[] {
if (isChunkStartLine(lines[line], isRDoc)) {
chunkId++;
chunkStartLine = line;
chunkFenceLength = isRDoc ? 0 : fenceLength(lines[line]);
chunkLanguage = getChunkLanguage(lines[line], isRDoc);
chunkOptions = getChunkOptions(lines[line], isRDoc);
chunkEval = getChunkEval(chunkOptions);
}
} else {
if (isChunkEndLine(lines[line], isRDoc)) {
if (isChunkEndLine(lines[line], isRDoc, chunkFenceLength)) {
finishChunk(isRDoc ? line - 1 : line, line - 1);
// An R marker ends the previous chunk and starts the next one,
// including when the marker is the document's final line.
Expand Down
38 changes: 38 additions & 0 deletions src/test/suite/chunks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,44 @@ suite('R code chunk boundaries', () => {
assert.deepStrictEqual(await chunkCode('```{r}\nx <- 1\n```', 'rmd'), ['x <- 1']);
});

test('keeps a longer outer fence open through a shorter inner example', async () => {
const text = '````{verbatim}\n\n```{r}\nfoo <- 1\n```\n\n````';
const document = await vscode.workspace.openTextDocument({
language: 'rmd',
content: text,
});
const chunks = getChunks(document);
assert.strictEqual(chunks.length, 1);
assert.strictEqual(chunks[0].language, 'verbatim');
assert.strictEqual(chunks[0].startLine, 0);
assert.strictEqual(chunks[0].endLine, 6);
assert.deepStrictEqual(document.getText(chunks[0].codeRange).split('\n'), [
'',
'```{r}',
'foo <- 1',
'```',
'',
]);
});

test('ends a chunk on a longer fence and resumes with the next real chunk', async () => {
assert.deepStrictEqual(await chunkCode('```{r}\nx <- 1\n````', 'rmd'), ['x <- 1']);
const nestedThenReal =
'````{verbatim}\n\n```{r}\nfoo <- 1\n```\n\n````\n```{r}\nx <- 1\n```';
assert.deepStrictEqual(await chunkCode(nestedThenReal, 'rmd'), [
'\n```{r}\nfoo <- 1\n```\n',
'x <- 1',
]);
});

test('ignores info strings, bare fences, and unclosed R Markdown chunks', async () => {
assert.deepStrictEqual(await chunkCode('```{r}\nx <- 1\n``` label\n```', 'rmd'), [
'x <- 1\n``` label',
]);
assert.deepStrictEqual(await chunkCode('```\ncode\n```', 'rmd'), []);
assert.deepStrictEqual(await chunkCode('```{r}\nx <- 1', 'rmd'), []);
});

test('section headers end an R chunk without dropping its preceding line', async () => {
assert.deepStrictEqual(await chunkCode('# %%\nx <- 1\n# section ----'), ['x <- 1']);
});
Expand Down
Loading