Skip to content

Commit 5c0e522

Browse files
authored
fix: improve editor responsiveness and session reliability (#1921)
* perf: avoid rescanning fragmented Interactive IPC frames * fix: cancel and clean up Interactive connection attempts * fix: restore help favorites and streamline viewer updates * perf: cache R chunks and avoid redundant editor work * perf: keep stalled terminals and long transcripts responsive * fix: make editor saves and CSV previews reliable * fix: settle and cancel language server startup failures * fix: load standard plots before the first resize event * test: keep unattended webview checks active and isolated * test: support dynamic formatting and isolate auto-closing fixtures * fix: use native CSV preview paths on Windows * fix: discard Help refreshes after navigation or close * fix: cancel language server startup through initialization * fix: bound startup waits and preserve graceful R shutdown * test: install styler for formatting integration * fix: let the latest Help refresh supersede older requests * fix: preserve Help navigation priority and history * fix: reveal Help after successful history navigation * fix: keep R Markdown chunk highlighting independent of CodeLens * test: budget Interactive restart scenarios separately
1 parent f81a4de commit 5c0e522

44 files changed

Lines changed: 3515 additions & 569 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/main.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
- name: Verify managed tool consent
4444
run: pnpm run test:tool-consent
4545
- name: Install R test dependencies
46-
run: install.packages(c("remotes", "pkgbuild", "languageserver", "lintr", "renv"))
46+
run: install.packages(c("remotes", "pkgbuild", "languageserver", "lintr", "styler", "renv"))
4747
shell: Rscript {0}
4848
- run: pnpm run build
4949
- name: Verify managed jgd preparation

‎.vscode-test.mjs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@ function testProfile(name, lspEnabled) {
2929
const directory = path.resolve('.vscode-test/user-data', name);
3030
mkdirSync(path.join(directory, 'User'), { recursive: true });
3131
writeFileSync(path.join(directory, 'User/settings.json'), JSON.stringify({ 'r.lsp.enabled': lspEnabled }));
32-
return ['--user-data-dir', directory];
32+
// Unattended runs can be occluded by another app. Keep browser paint and
33+
// resize callbacks active so webview assertions observe actual loading.
34+
return [
35+
'--user-data-dir', directory,
36+
'--disable-renderer-backgrounding',
37+
'--disable-background-timer-throttling',
38+
'--disable-backgrounding-occluded-windows'
39+
];
3340
}
3441

3542
export default defineConfig([

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Use Linux or macOS for Interactive runtime tests, with R and an installed arf 0.
8282
```sh
8383
export R_LIBS=/path/to/test-library
8484
mkdir -p "$R_LIBS"
85-
Rscript -e 'install.packages(c("remotes", "languageserver", "lintr", "renv"))'
85+
Rscript -e 'install.packages(c("remotes", "languageserver", "lintr", "styler", "renv"))'
8686
pnpm run build
8787
Rscript -e 'tinytest::test_package("sess")'
8888
```

‎src/completions.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,12 @@ export class HelpLinkHoverProvider implements vscode.HoverProvider {
222222
async provideHover(
223223
document: vscode.TextDocument,
224224
position: vscode.Position,
225+
cancellation?: vscode.CancellationToken,
225226
): Promise<vscode.Hover | null> {
226-
if (!config().get<boolean>('helpPanel.enableHoverLinks')) {
227+
if (
228+
cancellation?.isCancellationRequested ||
229+
!config().get<boolean>('helpPanel.enableHoverLinks')
230+
) {
227231
return null;
228232
}
229233

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

243247
const re = /([a-zA-Z0-9._:])+/;
244248
const wordRange = document.getWordRangeAtPosition(position, re);
249+
if (!wordRange) {
250+
return null;
251+
}
252+
const version = document.version;
245253
const token = document.getText(wordRange);
246254
const aliases = (await globalRHelp?.getMatchingAliases(token)) || [];
255+
if (
256+
!aliases.length ||
257+
cancellation?.isCancellationRequested ||
258+
document.isClosed ||
259+
document.version !== version
260+
) {
261+
return null;
262+
}
247263
const mds = aliases.map((a) => {
248264
const cmdText = `${a.package}::${a.alias}`;
249265
const args = [`/library/${a.package}/html/${a.name}.html`];

‎src/cppProperties.ts‎

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
import * as fs from 'fs';
44
import * as path from 'path';
55
import { Uri, window } from 'vscode';
6-
import { getRpath, getCurrentWorkspaceFolder, executeRCommand, createTempDir } from './util';
7-
import { execSync } from 'child_process';
6+
import {
7+
getRpath,
8+
getCurrentWorkspaceFolder,
9+
executeRCommand,
10+
createTempDir,
11+
spawnAsync,
12+
ToRStringLiteral,
13+
} from './util';
814
import { extensionContext } from './extension';
915

1016
export async function generateCppProperties(): Promise<void> {
@@ -52,25 +58,33 @@ async function generateCppPropertiesProc(workspaceFolder: string) {
5258
});
5359
}
5460

55-
const compileOutputCpp = collectCompilerOutput(rPath, workspaceFolder, 'cpp');
56-
const compileOutputC = collectCompilerOutput(rPath, workspaceFolder, 'c');
57-
58-
if (fs.existsSync(path.join(workspaceFolder, cleanupFile))) {
59-
await executeRCommand(`system("sh ./${cleanupFile}")`, workspaceFolder, (e: Error) => {
60-
void window.showErrorMessage(e.message);
61-
return '';
62-
});
61+
let compileOutputCpp: string;
62+
let compileOutputC: string;
63+
try {
64+
compileOutputCpp = await collectCompilerOutput(rPath, workspaceFolder, 'cpp');
65+
compileOutputC = await collectCompilerOutput(rPath, workspaceFolder, 'c');
66+
} finally {
67+
if (fs.existsSync(path.join(workspaceFolder, cleanupFile))) {
68+
await executeRCommand(`system("sh ./${cleanupFile}")`, workspaceFolder, (e: Error) => {
69+
void window.showErrorMessage(e.message);
70+
return '';
71+
});
72+
}
6373
}
6474

6575
const compileInfo = extractCompilerInfo(compileOutputCpp);
6676
const compileStdCpp = extractCompilerStd(compileOutputCpp);
6777
const compileStdC = extractCompilerStd(compileOutputC);
6878
const compileCall = extractCompilerCall(compileOutputCpp);
6979
const compilerPath = compileCall
70-
? await executeRCommand(`cat(Sys.which("${compileCall}"))`, workspaceFolder, (e: Error) => {
71-
void window.showErrorMessage(e.message);
72-
return '';
73-
})
80+
? await executeRCommand(
81+
`cat(Sys.which(${ToRStringLiteral(compileCall, '"')}))`,
82+
workspaceFolder,
83+
(e: Error) => {
84+
void window.showErrorMessage(e.message);
85+
return '';
86+
},
87+
)
7488
: '';
7589

7690
const intelliSensePlatform = platformChoose('windows', 'macos', 'linux');
@@ -134,10 +148,14 @@ async function collectRLinkingTo(workspaceFolder: string): Promise<string[]> {
134148
.asAbsolutePath('R/cppProperties/extractLinkingTo.R')
135149
.replace(/\\/g, '/');
136150
const linkingToIncludesStr = (
137-
await executeRCommand(`source('${rScript}')`, workspaceFolder, (e: Error) => {
138-
void window.showErrorMessage(e.message);
139-
return '';
140-
})
151+
await executeRCommand(
152+
`source(${ToRStringLiteral(rScript, '"')})`,
153+
workspaceFolder,
154+
(e: Error) => {
155+
void window.showErrorMessage(e.message);
156+
return '';
157+
},
158+
)
141159
)?.trim();
142160
if (!linkingToIncludesStr || linkingToIncludesStr === '') {
143161
return [];
@@ -190,37 +208,44 @@ function extractCompilerCall(compileOutput: string): string | undefined {
190208
}
191209

192210
const m = ccalls[1].match(rxComp);
193-
return m?.[1];
211+
return m ? ensureUnquoted(m[1]) : undefined;
194212
}
195213

196-
function collectCompilerOutput(rPath: string, workspaceFolder: string, testExtension: 'cpp' | 'c') {
214+
async function collectCompilerOutput(
215+
rPath: string,
216+
workspaceFolder: string,
217+
testExtension: 'cpp' | 'c',
218+
): Promise<string> {
197219
const makevarsFiles = ['Makevars', 'Makevars.win', 'Makevars.ucrt'];
198220

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

202-
// Copy makevars
203-
if (fs.existsSync(srcFolder)) {
204-
const projectMakevarsFiles = fs
205-
.readdirSync(srcFolder)
206-
.filter((fn) => makevarsFiles.includes(fn));
207-
for (const f of projectMakevarsFiles) {
208-
fs.copyFileSync(path.join(srcFolder, f), path.join(tempFolder, f));
224+
try {
225+
// Copy makevars
226+
if (fs.existsSync(srcFolder)) {
227+
const projectMakevarsFiles = fs
228+
.readdirSync(srcFolder)
229+
.filter((fn) => makevarsFiles.includes(fn));
230+
for (const f of projectMakevarsFiles) {
231+
fs.copyFileSync(path.join(srcFolder, f), path.join(tempFolder, f));
232+
}
209233
}
210-
}
211-
212-
// Create dummy source file
213-
const testFile = `comp_test.${testExtension}`;
214-
fs.writeFileSync(path.join(tempFolder, testFile), '');
215-
216-
// Compile dummy
217-
const command = `"${rPath}" CMD SHLIB ${testFile}`;
218-
const compileOutput = execSync(command, {
219-
cwd: tempFolder,
220-
}).toString();
221234

222-
// Cleanup
223-
fs.rmSync(tempFolder, { recursive: true, force: true });
224-
225-
return compileOutput;
235+
// Create dummy source file
236+
const testFile = `comp_test.${testExtension}`;
237+
fs.writeFileSync(path.join(tempFolder, testFile), '');
238+
239+
// Compile dummy
240+
const result = await spawnAsync(rPath, ['CMD', 'SHLIB', testFile], { cwd: tempFolder });
241+
if (result.status !== 0) {
242+
throw (
243+
result.error ??
244+
new Error(result.stderr || 'Could not determine R compiler settings.')
245+
);
246+
}
247+
return result.stdout;
248+
} finally {
249+
fs.rmSync(tempFolder, { recursive: true, force: true });
250+
}
226251
}

‎src/extension.ts‎

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -243,22 +243,24 @@ export async function activate(
243243

244244
// register on-enter rule for roxygen comments
245245
const wordPattern = /(-?\d*\.\d\w*)|([^`~!@$^&*()=+[{\]}\\|;:'",<>/\s]+)/g;
246-
vscode.languages.setLanguageConfiguration('r', {
247-
onEnterRules: [
248-
{
249-
// Automatically continue roxygen comments: #'
250-
action: { indentAction: vscode.IndentAction.None, appendText: "#' " },
251-
beforeText: /^\s*#'\s*[^\s]/, // matches a non-empty roxygen line
252-
},
253-
{
254-
// Automatically continue roxygen comments: #'
255-
action: { indentAction: vscode.IndentAction.None, appendText: "#' " },
256-
beforeText: /^\s*#'/, // matches any roxygen comment line, even an empty one
257-
previousLineText: /^\s*([^#\s].*|#[^'\s].*|#'\s*[^\s].*|)$/, // matches everything but an empty roxygen line
258-
},
259-
],
260-
wordPattern,
261-
});
246+
context.subscriptions.push(
247+
vscode.languages.setLanguageConfiguration('r', {
248+
onEnterRules: [
249+
{
250+
// Automatically continue roxygen comments: #'
251+
action: { indentAction: vscode.IndentAction.None, appendText: "#' " },
252+
beforeText: /^\s*#'\s*[^\s]/, // matches a non-empty roxygen line
253+
},
254+
{
255+
// Automatically continue roxygen comments: #'
256+
action: { indentAction: vscode.IndentAction.None, appendText: "#' " },
257+
beforeText: /^\s*#'/, // matches any roxygen comment line, even an empty one
258+
previousLineText: /^\s*([^#\s].*|#[^'\s].*|#'\s*[^\s].*|)$/, // matches everything but an empty roxygen line
259+
},
260+
],
261+
wordPattern,
262+
}),
263+
);
262264

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

281283
// register codelens and completion providers for r markdown and r files
282-
vscode.languages.registerCodeLensProvider(
283-
['r', 'rmd'],
284-
new rmarkdown.RMarkdownCodeLensProvider(),
284+
const codeLensProvider = new rmarkdown.RMarkdownCodeLensProvider();
285+
context.subscriptions.push(
286+
codeLensProvider,
287+
vscode.languages.registerCodeLensProvider(['r', 'rmd'], codeLensProvider),
288+
);
289+
context.subscriptions.push(
290+
vscode.languages.registerCompletionItemProvider(
291+
'rmd',
292+
new rmarkdown.RMarkdownCompletionItemProvider(),
293+
' ',
294+
',',
295+
),
285296
);
286-
vscode.languages.registerCompletionItemProvider(
287-
'rmd',
288-
new rmarkdown.RMarkdownCompletionItemProvider(),
289-
' ',
290-
',',
297+
context.subscriptions.push(
298+
vscode.languages.registerFoldingRangeProvider('r', new rmarkdown.RChunkFoldingProvider()),
291299
);
292-
vscode.languages.registerFoldingRangeProvider('r', new rmarkdown.RChunkFoldingProvider());
293300

294301
// register (session) hover and completion providers
295-
vscode.languages.registerHoverProvider(['r', 'rmd'], new completions.HoverProvider());
296-
vscode.languages.registerHoverProvider(['r', 'rmd'], new completions.HelpLinkHoverProvider());
297-
vscode.languages.registerCompletionItemProvider(
298-
['r', 'rmd'],
299-
new completions.StaticCompletionItemProvider(),
300-
'@',
302+
context.subscriptions.push(
303+
vscode.languages.registerHoverProvider(['r', 'rmd'], new completions.HoverProvider()),
304+
);
305+
context.subscriptions.push(
306+
vscode.languages.registerHoverProvider(
307+
['r', 'rmd'],
308+
new completions.HelpLinkHoverProvider(),
309+
),
310+
);
311+
context.subscriptions.push(
312+
vscode.languages.registerCompletionItemProvider(
313+
['r', 'rmd'],
314+
new completions.StaticCompletionItemProvider(),
315+
'@',
316+
),
301317
);
302318

303319
// register task provider
304320
const taskProvider = new RTaskProvider();
305-
vscode.tasks.registerTaskProvider(taskProvider.type, taskProvider);
321+
context.subscriptions.push(vscode.tasks.registerTaskProvider(taskProvider.type, taskProvider));
306322

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

322338
// track active text editor
323339
rstudioapi.trackLastActiveTextEditor(vscode.window.activeTextEditor);
324-
vscode.window.onDidChangeActiveTextEditor(rstudioapi.trackLastActiveTextEditor);
340+
context.subscriptions.push(
341+
vscode.window.onDidChangeActiveTextEditor(rstudioapi.trackLastActiveTextEditor),
342+
);
325343

326344
// register the R Workspace tree view
327345
// creates a custom context value for the workspace view

0 commit comments

Comments
 (0)