Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c4dace0
perf: avoid rescanning fragmented Interactive IPC frames
renkun-ken Oct 30, 2026
231ae34
fix: cancel and clean up Interactive connection attempts
renkun-ken Oct 30, 2026
596a636
fix: restore help favorites and streamline viewer updates
renkun-ken Oct 30, 2026
588a11a
perf: cache R chunks and avoid redundant editor work
renkun-ken Oct 30, 2026
b76c359
perf: keep stalled terminals and long transcripts responsive
renkun-ken Oct 30, 2026
81907b2
fix: make editor saves and CSV previews reliable
renkun-ken Oct 30, 2026
4e0a7c9
fix: settle and cancel language server startup failures
renkun-ken Oct 30, 2026
89bef74
fix: load standard plots before the first resize event
renkun-ken Oct 30, 2026
11507bd
test: keep unattended webview checks active and isolated
renkun-ken Oct 30, 2026
15ea1f3
test: support dynamic formatting and isolate auto-closing fixtures
renkun-ken Oct 30, 2026
345c781
fix: use native CSV preview paths on Windows
renkun-ken Oct 30, 2026
4c012e7
fix: discard Help refreshes after navigation or close
renkun-ken Nov 6, 2026
edeb102
fix: cancel language server startup through initialization
renkun-ken Nov 6, 2026
dbb6b89
fix: bound startup waits and preserve graceful R shutdown
renkun-ken Nov 6, 2026
0173795
test: install styler for formatting integration
renkun-ken Nov 6, 2026
47bcb3c
fix: let the latest Help refresh supersede older requests
renkun-ken Nov 6, 2026
13d4400
fix: preserve Help navigation priority and history
renkun-ken Nov 6, 2026
980b7a0
fix: reveal Help after successful history navigation
renkun-ken Nov 6, 2026
017dccd
fix: keep R Markdown chunk highlighting independent of CodeLens
renkun-ken Nov 6, 2026
99f915d
test: budget Interactive restart scenarios separately
renkun-ken Nov 6, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Verify managed tool consent
run: pnpm run test:tool-consent
- name: Install R test dependencies
run: install.packages(c("remotes", "pkgbuild", "languageserver", "lintr", "renv"))
run: install.packages(c("remotes", "pkgbuild", "languageserver", "lintr", "styler", "renv"))
shell: Rscript {0}
- run: pnpm run build
- name: Verify managed jgd preparation
Expand Down
9 changes: 8 additions & 1 deletion .vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ function testProfile(name, lspEnabled) {
const directory = path.resolve('.vscode-test/user-data', name);
mkdirSync(path.join(directory, 'User'), { recursive: true });
writeFileSync(path.join(directory, 'User/settings.json'), JSON.stringify({ 'r.lsp.enabled': lspEnabled }));
return ['--user-data-dir', directory];
// Unattended runs can be occluded by another app. Keep browser paint and
// resize callbacks active so webview assertions observe actual loading.
return [
'--user-data-dir', directory,
'--disable-renderer-backgrounding',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows'
];
}

export default defineConfig([
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Use Linux or macOS for Interactive runtime tests, with R and an installed arf 0.
```sh
export R_LIBS=/path/to/test-library
mkdir -p "$R_LIBS"
Rscript -e 'install.packages(c("remotes", "languageserver", "lintr", "renv"))'
Rscript -e 'install.packages(c("remotes", "languageserver", "lintr", "styler", "renv"))'
pnpm run build
Rscript -e 'tinytest::test_package("sess")'
```
Expand Down
18 changes: 17 additions & 1 deletion src/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,12 @@ export class HelpLinkHoverProvider implements vscode.HoverProvider {
async provideHover(
document: vscode.TextDocument,
position: vscode.Position,
cancellation?: vscode.CancellationToken,
): Promise<vscode.Hover | null> {
if (!config().get<boolean>('helpPanel.enableHoverLinks')) {
if (
cancellation?.isCancellationRequested ||
!config().get<boolean>('helpPanel.enableHoverLinks')
) {
return null;
}

Expand All @@ -242,8 +246,20 @@ export class HelpLinkHoverProvider implements vscode.HoverProvider {

const re = /([a-zA-Z0-9._:])+/;
const wordRange = document.getWordRangeAtPosition(position, re);
if (!wordRange) {
return null;
}
const version = document.version;
const token = document.getText(wordRange);
const aliases = (await globalRHelp?.getMatchingAliases(token)) || [];
if (
!aliases.length ||
cancellation?.isCancellationRequested ||
document.isClosed ||
document.version !== version
) {
return null;
}
const mds = aliases.map((a) => {
const cmdText = `${a.package}::${a.alias}`;
const args = [`/library/${a.package}/html/${a.name}.html`];
Expand Down
109 changes: 67 additions & 42 deletions src/cppProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
import * as fs from 'fs';
import * as path from 'path';
import { Uri, window } from 'vscode';
import { getRpath, getCurrentWorkspaceFolder, executeRCommand, createTempDir } from './util';
import { execSync } from 'child_process';
import {
getRpath,
getCurrentWorkspaceFolder,
executeRCommand,
createTempDir,
spawnAsync,
ToRStringLiteral,
} from './util';
import { extensionContext } from './extension';

export async function generateCppProperties(): Promise<void> {
Expand Down Expand Up @@ -52,25 +58,33 @@ async function generateCppPropertiesProc(workspaceFolder: string) {
});
}

const compileOutputCpp = collectCompilerOutput(rPath, workspaceFolder, 'cpp');
const compileOutputC = collectCompilerOutput(rPath, workspaceFolder, 'c');

if (fs.existsSync(path.join(workspaceFolder, cleanupFile))) {
await executeRCommand(`system("sh ./${cleanupFile}")`, workspaceFolder, (e: Error) => {
void window.showErrorMessage(e.message);
return '';
});
let compileOutputCpp: string;
let compileOutputC: string;
try {
compileOutputCpp = await collectCompilerOutput(rPath, workspaceFolder, 'cpp');
compileOutputC = await collectCompilerOutput(rPath, workspaceFolder, 'c');
} finally {
if (fs.existsSync(path.join(workspaceFolder, cleanupFile))) {
await executeRCommand(`system("sh ./${cleanupFile}")`, workspaceFolder, (e: Error) => {
void window.showErrorMessage(e.message);
return '';
});
}
}

const compileInfo = extractCompilerInfo(compileOutputCpp);
const compileStdCpp = extractCompilerStd(compileOutputCpp);
const compileStdC = extractCompilerStd(compileOutputC);
const compileCall = extractCompilerCall(compileOutputCpp);
const compilerPath = compileCall
? await executeRCommand(`cat(Sys.which("${compileCall}"))`, workspaceFolder, (e: Error) => {
void window.showErrorMessage(e.message);
return '';
})
? await executeRCommand(
`cat(Sys.which(${ToRStringLiteral(compileCall, '"')}))`,
workspaceFolder,
(e: Error) => {
void window.showErrorMessage(e.message);
return '';
},
)
: '';

const intelliSensePlatform = platformChoose('windows', 'macos', 'linux');
Expand Down Expand Up @@ -134,10 +148,14 @@ async function collectRLinkingTo(workspaceFolder: string): Promise<string[]> {
.asAbsolutePath('R/cppProperties/extractLinkingTo.R')
.replace(/\\/g, '/');
const linkingToIncludesStr = (
await executeRCommand(`source('${rScript}')`, workspaceFolder, (e: Error) => {
void window.showErrorMessage(e.message);
return '';
})
await executeRCommand(
`source(${ToRStringLiteral(rScript, '"')})`,
workspaceFolder,
(e: Error) => {
void window.showErrorMessage(e.message);
return '';
},
)
)?.trim();
if (!linkingToIncludesStr || linkingToIncludesStr === '') {
return [];
Expand Down Expand Up @@ -190,37 +208,44 @@ function extractCompilerCall(compileOutput: string): string | undefined {
}

const m = ccalls[1].match(rxComp);
return m?.[1];
return m ? ensureUnquoted(m[1]) : undefined;
}

function collectCompilerOutput(rPath: string, workspaceFolder: string, testExtension: 'cpp' | 'c') {
async function collectCompilerOutput(
rPath: string,
workspaceFolder: string,
testExtension: 'cpp' | 'c',
): Promise<string> {
const makevarsFiles = ['Makevars', 'Makevars.win', 'Makevars.ucrt'];

const srcFolder = path.join(workspaceFolder, 'src');
const tempFolder = createTempDir(workspaceFolder);

// Copy makevars
if (fs.existsSync(srcFolder)) {
const projectMakevarsFiles = fs
.readdirSync(srcFolder)
.filter((fn) => makevarsFiles.includes(fn));
for (const f of projectMakevarsFiles) {
fs.copyFileSync(path.join(srcFolder, f), path.join(tempFolder, f));
try {
// Copy makevars
if (fs.existsSync(srcFolder)) {
const projectMakevarsFiles = fs
.readdirSync(srcFolder)
.filter((fn) => makevarsFiles.includes(fn));
for (const f of projectMakevarsFiles) {
fs.copyFileSync(path.join(srcFolder, f), path.join(tempFolder, f));
}
}
}

// Create dummy source file
const testFile = `comp_test.${testExtension}`;
fs.writeFileSync(path.join(tempFolder, testFile), '');

// Compile dummy
const command = `"${rPath}" CMD SHLIB ${testFile}`;
const compileOutput = execSync(command, {
cwd: tempFolder,
}).toString();

// Cleanup
fs.rmSync(tempFolder, { recursive: true, force: true });

return compileOutput;
// Create dummy source file
const testFile = `comp_test.${testExtension}`;
fs.writeFileSync(path.join(tempFolder, testFile), '');

// Compile dummy
const result = await spawnAsync(rPath, ['CMD', 'SHLIB', testFile], { cwd: tempFolder });
if (result.status !== 0) {
throw (
result.error ??
new Error(result.stderr || 'Could not determine R compiler settings.')
);
}
return result.stdout;
} finally {
fs.rmSync(tempFolder, { recursive: true, force: true });
}
}
84 changes: 51 additions & 33 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,22 +243,24 @@ export async function activate(

// register on-enter rule for roxygen comments
const wordPattern = /(-?\d*\.\d\w*)|([^`~!@$^&*()=+[{\]}\\|;:'",<>/\s]+)/g;
vscode.languages.setLanguageConfiguration('r', {
onEnterRules: [
{
// Automatically continue roxygen comments: #'
action: { indentAction: vscode.IndentAction.None, appendText: "#' " },
beforeText: /^\s*#'\s*[^\s]/, // matches a non-empty roxygen line
},
{
// Automatically continue roxygen comments: #'
action: { indentAction: vscode.IndentAction.None, appendText: "#' " },
beforeText: /^\s*#'/, // matches any roxygen comment line, even an empty one
previousLineText: /^\s*([^#\s].*|#[^'\s].*|#'\s*[^\s].*|)$/, // matches everything but an empty roxygen line
},
],
wordPattern,
});
context.subscriptions.push(
vscode.languages.setLanguageConfiguration('r', {
onEnterRules: [
{
// Automatically continue roxygen comments: #'
action: { indentAction: vscode.IndentAction.None, appendText: "#' " },
beforeText: /^\s*#'\s*[^\s]/, // matches a non-empty roxygen line
},
{
// Automatically continue roxygen comments: #'
action: { indentAction: vscode.IndentAction.None, appendText: "#' " },
beforeText: /^\s*#'/, // matches any roxygen comment line, even an empty one
previousLineText: /^\s*([^#\s].*|#[^'\s].*|#'\s*[^\s].*|)$/, // matches everything but an empty roxygen line
},
],
wordPattern,
}),
);

// register terminal-provider
context.subscriptions.push(
Expand All @@ -279,30 +281,44 @@ export async function activate(
context.subscriptions.push(new InteractiveManager(context));

// register codelens and completion providers for r markdown and r files
vscode.languages.registerCodeLensProvider(
['r', 'rmd'],
new rmarkdown.RMarkdownCodeLensProvider(),
const codeLensProvider = new rmarkdown.RMarkdownCodeLensProvider();
context.subscriptions.push(
codeLensProvider,
vscode.languages.registerCodeLensProvider(['r', 'rmd'], codeLensProvider),
);
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
'rmd',
new rmarkdown.RMarkdownCompletionItemProvider(),
' ',
',',
),
);
vscode.languages.registerCompletionItemProvider(
'rmd',
new rmarkdown.RMarkdownCompletionItemProvider(),
' ',
',',
context.subscriptions.push(
vscode.languages.registerFoldingRangeProvider('r', new rmarkdown.RChunkFoldingProvider()),
);
vscode.languages.registerFoldingRangeProvider('r', new rmarkdown.RChunkFoldingProvider());

// register (session) hover and completion providers
vscode.languages.registerHoverProvider(['r', 'rmd'], new completions.HoverProvider());
vscode.languages.registerHoverProvider(['r', 'rmd'], new completions.HelpLinkHoverProvider());
vscode.languages.registerCompletionItemProvider(
['r', 'rmd'],
new completions.StaticCompletionItemProvider(),
'@',
context.subscriptions.push(
vscode.languages.registerHoverProvider(['r', 'rmd'], new completions.HoverProvider()),
);
context.subscriptions.push(
vscode.languages.registerHoverProvider(
['r', 'rmd'],
new completions.HelpLinkHoverProvider(),
),
);
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
['r', 'rmd'],
new completions.StaticCompletionItemProvider(),
'@',
),
);

// register task provider
const taskProvider = new RTaskProvider();
vscode.tasks.registerTaskProvider(taskProvider.type, taskProvider);
context.subscriptions.push(vscode.tasks.registerTaskProvider(taskProvider.type, taskProvider));

// deploy session watcher (if configured by user)
if (enableSessionWatcher) {
Expand All @@ -321,7 +337,9 @@ export async function activate(

// track active text editor
rstudioapi.trackLastActiveTextEditor(vscode.window.activeTextEditor);
vscode.window.onDidChangeActiveTextEditor(rstudioapi.trackLastActiveTextEditor);
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(rstudioapi.trackLastActiveTextEditor),
);

// register the R Workspace tree view
// creates a custom context value for the workspace view
Expand Down
Loading
Loading