From 60b00e99b2e4c0b8b3c1dd626be07202c8009366 Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 11 Oct 2026 20:14:01 +0800 Subject: [PATCH 1/3] fix: clear stale chunk decorations --- src/rmarkdown/index.ts | 3 ++ src/test/suite/rmarkdownDecoration.test.ts | 40 ++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/test/suite/rmarkdownDecoration.test.ts diff --git a/src/rmarkdown/index.ts b/src/rmarkdown/index.ts index 061226696..2ed6f8932 100644 --- a/src/rmarkdown/index.ts +++ b/src/rmarkdown/index.ts @@ -102,6 +102,9 @@ export class RMarkdownCodeLensProvider implements vscode.CodeLensProvider { ), ); editor.setDecorations(this.currentCellBottom, [currentChunkEnd]); + } else { + editor.setDecorations(this.currentCellTop, []); + editor.setDecorations(this.currentCellBottom, []); } } } diff --git a/src/test/suite/rmarkdownDecoration.test.ts b/src/test/suite/rmarkdownDecoration.test.ts new file mode 100644 index 000000000..da331bef4 --- /dev/null +++ b/src/test/suite/rmarkdownDecoration.test.ts @@ -0,0 +1,40 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import sinon = require('sinon'); + +import { RMarkdownCodeLensProvider } from '../../rmarkdown'; + +suite('R Markdown chunk decorations', () => { + test('clears current chunk borders when the document has no chunks', async () => { + const document = await vscode.workspace.openTextDocument({ + language: 'r', + content: '# %%\nx <- 1\n# %%\ny <- 2' + }); + const editor = await vscode.window.showTextDocument(document); + editor.selection = new vscode.Selection(1, 0, 1, 0); + + const provider = new RMarkdownCodeLensProvider(); + const setDecorations = sinon.spy(editor, 'setDecorations'); + try { + provider.provideCodeLenses(document, vscode.CancellationToken.None); + assert.strictEqual(setDecorations.callCount, 2); + + const edit = new vscode.WorkspaceEdit(); + edit.delete(document.uri, new vscode.Range( + new vscode.Position(0, 0), + document.lineAt(document.lineCount - 1).range.end + )); + assert.strictEqual(await vscode.workspace.applyEdit(edit), true); + + setDecorations.resetHistory(); + provider.provideCodeLenses(document, vscode.CancellationToken.None); + assert.deepStrictEqual( + setDecorations.getCalls().map(call => call.args[1]), + [[], []] + ); + } finally { + setDecorations.restore(); + provider.dispose(); + } + }); +}); From a71850e82a7e5826276c254d7e8dd530028e8c20 Mon Sep 17 00:00:00 2001 From: Sean Li Date: Fri, 16 Oct 2026 19:01:15 +0800 Subject: [PATCH 2/3] test: fix chunk decoration regression coverage --- src/rmarkdown/index.ts | 9 +++-- src/test/suite/rmarkdownDecoration.test.ts | 42 +++++++++++++--------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/rmarkdown/index.ts b/src/rmarkdown/index.ts index 2ed6f8932..4fa2818f4 100644 --- a/src/rmarkdown/index.ts +++ b/src/rmarkdown/index.ts @@ -36,7 +36,10 @@ export class RMarkdownCodeLensProvider implements vscode.CodeLensProvider { private readonly currentCellBottom: vscode.TextEditorDecorationType; private onDidChangeTextEditorSelectionHandler: vscode.Disposable | undefined; - constructor() { + constructor( + private readonly getVisibleTextEditors: () => readonly vscode.TextEditor[] = () => + vscode.window.visibleTextEditors, + ) { this.decoration = vscode.window.createTextEditorDecorationType({ isWholeLine: true, backgroundColor: config().get('rmarkdown.chunkBackgroundColor'), @@ -76,7 +79,7 @@ export class RMarkdownCodeLensProvider implements vscode.CodeLensProvider { } private highlightCurrentChunk(chunks: RMarkdownChunk[], document: vscode.TextDocument) { - for (const editor of vscode.window.visibleTextEditors) { + for (const editor of this.getVisibleTextEditors()) { if (editor.document.uri.toString() === document.uri.toString()) { const lines = document.getText().split(/\r?\n/); const currentLine = editor.selection.active.line; @@ -112,7 +115,7 @@ export class RMarkdownCodeLensProvider implements vscode.CodeLensProvider { private highlightChunks(chunks: RMarkdownChunk[], document: vscode.TextDocument) { const chunkRanges = chunks.map((chunk) => chunk.chunkRange); - for (const editor of vscode.window.visibleTextEditors) { + for (const editor of this.getVisibleTextEditors()) { if (editor.document.uri.toString() === document.uri.toString()) { editor.setDecorations(this.decoration, chunkRanges); } diff --git a/src/test/suite/rmarkdownDecoration.test.ts b/src/test/suite/rmarkdownDecoration.test.ts index da331bef4..24bfebc16 100644 --- a/src/test/suite/rmarkdownDecoration.test.ts +++ b/src/test/suite/rmarkdownDecoration.test.ts @@ -1,6 +1,5 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; -import sinon = require('sinon'); import { RMarkdownCodeLensProvider } from '../../rmarkdown'; @@ -8,32 +7,41 @@ suite('R Markdown chunk decorations', () => { test('clears current chunk borders when the document has no chunks', async () => { const document = await vscode.workspace.openTextDocument({ language: 'r', - content: '# %%\nx <- 1\n# %%\ny <- 2' + content: '# %%\nx <- 1\n# %%\ny <- 2', }); const editor = await vscode.window.showTextDocument(document); editor.selection = new vscode.Selection(1, 0, 1, 0); - const provider = new RMarkdownCodeLensProvider(); - const setDecorations = sinon.spy(editor, 'setDecorations'); + const decorationCalls: Array = []; + const editorWrapper = Object.create(editor) as vscode.TextEditor; + Object.defineProperty(editorWrapper, 'setDecorations', { + configurable: true, + value: ( + _decorationType: vscode.TextEditorDecorationType, + ranges: readonly vscode.Range[], + ) => { + decorationCalls.push(ranges); + }, + }); + const provider = new RMarkdownCodeLensProvider(() => [editorWrapper]); try { - provider.provideCodeLenses(document, vscode.CancellationToken.None); - assert.strictEqual(setDecorations.callCount, 2); + provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); + assert.strictEqual(decorationCalls.length, 2); const edit = new vscode.WorkspaceEdit(); - edit.delete(document.uri, new vscode.Range( - new vscode.Position(0, 0), - document.lineAt(document.lineCount - 1).range.end - )); + edit.delete( + document.uri, + new vscode.Range( + new vscode.Position(0, 0), + document.lineAt(document.lineCount - 1).range.end, + ), + ); assert.strictEqual(await vscode.workspace.applyEdit(edit), true); - setDecorations.resetHistory(); - provider.provideCodeLenses(document, vscode.CancellationToken.None); - assert.deepStrictEqual( - setDecorations.getCalls().map(call => call.args[1]), - [[], []] - ); + decorationCalls.length = 0; + provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); + assert.deepStrictEqual(decorationCalls, [[], []]); } finally { - setDecorations.restore(); provider.dispose(); } }); From e8974b07fa3a9015bf43a0445f6876a8d22e39dc Mon Sep 17 00:00:00 2001 From: Sean Li Date: Wed, 21 Oct 2026 20:39:39 +0800 Subject: [PATCH 3/3] test: isolate R Markdown decoration test editor --- src/test/suite/rmarkdownDecoration.test.ts | 41 +++++++--------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/src/test/suite/rmarkdownDecoration.test.ts b/src/test/suite/rmarkdownDecoration.test.ts index 24bfebc16..0f756cfb5 100644 --- a/src/test/suite/rmarkdownDecoration.test.ts +++ b/src/test/suite/rmarkdownDecoration.test.ts @@ -5,40 +5,23 @@ import { RMarkdownCodeLensProvider } from '../../rmarkdown'; suite('R Markdown chunk decorations', () => { test('clears current chunk borders when the document has no chunks', async () => { - const document = await vscode.workspace.openTextDocument({ - language: 'r', - content: '# %%\nx <- 1\n# %%\ny <- 2', - }); - const editor = await vscode.window.showTextDocument(document); - editor.selection = new vscode.Selection(1, 0, 1, 0); + const document = { + languageId: 'r', + uri: vscode.Uri.parse('untitled:rmarkdown-decoration-test'), + getText: () => 'x <- 1', + } as vscode.TextDocument; const decorationCalls: Array = []; - const editorWrapper = Object.create(editor) as vscode.TextEditor; - Object.defineProperty(editorWrapper, 'setDecorations', { - configurable: true, - value: ( + const editor = { + document, + selection: new vscode.Selection(0, 0, 0, 0), + setDecorations: ( _decorationType: vscode.TextEditorDecorationType, ranges: readonly vscode.Range[], - ) => { - decorationCalls.push(ranges); - }, - }); - const provider = new RMarkdownCodeLensProvider(() => [editorWrapper]); + ) => decorationCalls.push(ranges), + } as unknown as vscode.TextEditor; + const provider = new RMarkdownCodeLensProvider(() => [editor]); try { - provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); - assert.strictEqual(decorationCalls.length, 2); - - const edit = new vscode.WorkspaceEdit(); - edit.delete( - document.uri, - new vscode.Range( - new vscode.Position(0, 0), - document.lineAt(document.lineCount - 1).range.end, - ), - ); - assert.strictEqual(await vscode.workspace.applyEdit(edit), true); - - decorationCalls.length = 0; provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); assert.deepStrictEqual(decorationCalls, [[], []]); } finally {