Skip to content
Open
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
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
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
45 changes: 34 additions & 11 deletions src/helpViewer/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export class PackageManager {
// public favoriteNames: string[] = [];

public favoriteNames: Set<string> = new Set();
private readonly indexLoads = new Map<string, Promise<IndexEntry[] | undefined>>();
private cacheGeneration = 0;

constructor(args: PackageManagerOptions) {
this.rHelp = args.rHelp;
Expand All @@ -98,6 +100,8 @@ export class PackageManager {

// Funciton to clear only the cached files regarding an individual package etc.
public async clearCachedFiles(re?: string | RegExp): Promise<void> {
this.cacheGeneration++;
this.indexLoads.clear();
let cache: CachedIndexFiles | undefined;
if (re) {
const oldCache = this.state.get<CachedIndexFiles>('r.helpPanel.cachedIndexFiles', []);
Expand Down Expand Up @@ -161,13 +165,21 @@ export class PackageManager {
// Is used frequently when list of favorites is shared globally to sync between sessions
private pullFavoriteNames() {
if (this.state) {
this.favoriteNames =
this.state.get('r.helpPanel.favoriteNamesSet') || this.favoriteNames;
const saved = this.state.get<unknown>('r.helpPanel.favoriteNamesSet');
if (saved !== undefined) {
// Memento persists JSON. Older versions saved a Set, which becomes
// an empty object after reloading the extension host.
this.favoriteNames = new Set(
(Array.isArray(saved) ? saved : saved instanceof Set ? [...saved] : []).filter(
(name): name is string => typeof name === 'string',
),
);
}
}
}
private pushFavoriteNames() {
if (this.state) {
void this.state.update('r.helpPanel.favoriteNamesSet', this.favoriteNames);
void this.state.update('r.helpPanel.favoriteNamesSet', [...this.favoriteNames]);
}
}

Expand Down Expand Up @@ -375,15 +387,26 @@ export class PackageManager {

// only read and parse file if not cached yet
if (!indexItems) {
const helpFile = await this.rHelp.getHelpFileForPath(path, false);
if (!helpFile?.html) {
// set missing files to null
indexItems = undefined;
} else {
// parse and cache file
indexItems = parseIndexFile(helpFile.html);
let load = this.indexLoads.get(path);
if (!load) {
const generation = this.cacheGeneration;
load = (async () => {
const helpFile = await this.rHelp.getHelpFileForPath(path, false);
const items = helpFile?.html ? parseIndexFile(helpFile.html) : undefined;
if (generation === this.cacheGeneration) {
await this.updateCachedIndexFile(path, items);
}
return items;
})();
this.indexLoads.set(path, load);
}
try {
indexItems = await load;
} finally {
if (this.indexLoads.get(path) === load) {
this.indexLoads.delete(path);
}
}
void this.updateCachedIndexFile(path, indexItems);
}

// return cache entry. make new array to avoid messing with the cache
Expand Down
Loading
Loading