Skip to content

Commit 8a6db8d

Browse files
committed
[release:patch] Improve hover-over and package presentation
1 parent 6c1312a commit 8a6db8d

9 files changed

Lines changed: 304 additions & 79 deletions

File tree

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ With this extension, you gain access to the following features (as the extension
5858

5959
</details>
6060

61+
7. 📦 [**Package Information**](#package-information): Hover over a call to see which package it comes from and its version from flowR's bundled [package database](#package-database), and get a [Project](#project-view) overview of your declared libraries.
62+
6163
If you notice anything that could be improved, have a feature request, or notice a bug, please [open an issue](#issues-and-feature-requests)!
6264

6365
<details><summary>Reporting an Issue</summary>
@@ -113,7 +115,7 @@ Additionally, we recommend using the [R extension](https://marketplace.visualstu
113115

114116
### Linting
115117

116-
By default, the extension ships with all linting rules that are available in *flowR*, extensive information about which can be found on the [wiki page](https://github.com/flowr-analysis/flowr/wiki/Linter). Some linters additionally ship with code actions in the form of quick fixes which, once invoked, automatically edit or remove the relevant code snippet.
118+
By default, the extension enables all linting rules available in *flowR* except `naming-convention` and `roxygen-arguments`; extensive information about the rules can be found on the [wiki page](https://github.com/flowr-analysis/flowr/wiki/Linter). The package-structure rules `software-has-license` and `software-has-tests` are only applied when the file is part of an R package (i.e. a `DESCRIPTION` file is present up the directory tree), so they do not add noise to standalone scripts. You can adjust the enabled rules under `vscode-flowr.linter.enabledRules`. Some linters additionally ship with code actions in the form of quick fixes which, once invoked, automatically edit or remove the relevant code snippet.
117119

118120
To manually invoke the linter, you can use the "Code Quality Analysis (Linter)" command. Additionally, you can modify under what conditions the linter automatically refreshes its results in the extension's settings.
119121

@@ -158,7 +160,19 @@ To clear the slice highlighting, use the "Clear Current Slice Presentation" comm
158160

159161
![A screenshot of a dependency diagram for a piece of code](https://raw.githubusercontent.com/flowr-analysis/vscode-flowr/refs/heads/main/media/dependencies.png)
160162

161-
Using the extension, the sidebar should contain a flowR icon which holds more information on the current file, listing the libraries loaded, the files read and written, and the sourced scripts. If you expand the respective sections, clicking on the found entries should open them in the editor. The context menu (available with a right click) allows you to [slice](#slicing) for the selected entry.
163+
Using the extension, the sidebar should contain a flowR icon whose **Overview** view holds more information on the current file, listing the libraries loaded, the files read and written, and the sourced scripts. If you expand the respective sections, clicking on the found entries should open them in the editor. The context menu (available with a right click) allows you to [slice](#slicing) for the selected entry.
164+
165+
### Package Information
166+
167+
Hovering over a call in an R file tells you which package it stems from — for example, hovering over `map` after `library(purrr)` shows *`map` is provided by the `purrr` package*, together with the version recorded in flowR's [package database](#package-database) and a link to the package's CRAN page. Hovering over the package name inside `library(pkg)`/`require(pkg)` shows that package's database version and CRAN link. For locally defined functions and variables, <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+click (Go to Definition) jumps to their definition.
168+
169+
### Project View
170+
171+
When the open workspace contains an R project manifest — a `renv.lock`, a `DESCRIPTION`, or an `rv.lock`/`rproject.toml` — a **Project** tab appears in the flowR sidebar. It lists the libraries each manifest declares and, for every one, whether flowR's [package database](#package-database) knows it: matched (with the database version), a base package bundled with R, unmatched, or unavailable. The tab is hidden when no manifest is detected.
172+
173+
### Package Database
174+
175+
flowR resolves the exports, definitions and versions of external packages from a precomputed *package database* that ships with the extension, so package attribution works out of the box without a local R installation. It powers the [package information](#package-information) hovers, the [project view](#project-view) matching, and dependency resolution. The active database (scope and date) is shown when hovering the flowR status-bar item and in the REPL banner. You can toggle it or point at a custom database under the `vscode-flowr.config.solver.pkgdb.*` settings.
162176

163177
### Value Resolution
164178

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,7 @@
805805
{
806806
"id": "flowr-dependencies",
807807
"icon": "$(notebook-template)",
808-
"name": "Dependencies"
808+
"name": "Overview"
809809
},
810810
{
811811
"id": "flowr-project",

src/flowr/internal-session.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,16 +404,29 @@ export class FlowrInternalSession implements FlowrSession {
404404
});
405405
}
406406

407-
public async runRepl(config: Omit<Required<FlowrReplOptions>, 'parser'>) {
407+
/**
408+
* Builds the analyzer the REPL runs on (validating the flowR configuration) and the banner shown on start,
409+
* which includes the flowR/engine versions and the active package database. Split out from {@link runRepl}
410+
* so it can be exercised in tests without entering the interactive read loop (which would end the process).
411+
*/
412+
public async replStartup(): Promise<{ analyzer: FlowrAnalyzer, banner: string } | undefined> {
408413
if(!this.parser) {
409-
return;
414+
return undefined;
410415
}
411416
const analyzer = await new FlowrAnalyzerBuilder()
412417
.setParser(this.parser)
413418
.setConfig(VSCodeFlowrConfiguration)
414419
.build();
415-
(config.output as { stdout: (s: string) => void }).stdout(`${await versionReplString(this.parser)}\n${packageDbSummary()}`);
416-
await repl({ analyzer: analyzer, ...config });
420+
return { analyzer, banner: `${await versionReplString(this.parser)}\n${packageDbSummary()}` };
421+
}
422+
423+
public async runRepl(config: Omit<Required<FlowrReplOptions>, 'parser'>) {
424+
const startup = await this.replStartup();
425+
if(!startup) {
426+
return;
427+
}
428+
(config.output as { stdout: (s: string) => void }).stdout(startup.banner);
429+
await repl({ analyzer: startup.analyzer, ...config });
417430
}
418431

419432
public static getEngineToUse(): KnownParserName {

src/flowr/views/project-view.ts

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,13 @@
11
import * as vscode from 'vscode';
22
import { Settings } from '../../settings';
33
import type { PkgDatabase } from '@eagleoutice/flowr/project/plugins/package-version-plugins/pkgdb';
4-
import { getPackageDatabase } from '../../package-db';
4+
import { getPackageDatabase, baseRPackages } from '../../package-db';
55

66
export const FlowrProjectViewId = 'flowr-project';
77

88
/** context key backing the `when` clause that hides the Project tab until a manifest is found */
99
const ProjectContextKey = 'vscode-flowr:hasProject';
1010

11-
/**
12-
* Packages that ship with R itself (the `base` and `recommended` priority packages). They are never part
13-
* of the CRAN package database, so we must not flag them as "unmatched" - they are simply always present.
14-
*/
15-
const BaseAndRecommendedPackages = new Set([
16-
// base
17-
'base', 'compiler', 'datasets', 'grDevices', 'graphics', 'grid', 'methods', 'parallel', 'splines',
18-
'stats', 'stats4', 'tcltk', 'tools', 'translations', 'utils',
19-
// recommended
20-
'boot', 'class', 'cluster', 'codetools', 'foreign', 'KernSmooth', 'lattice', 'MASS', 'Matrix', 'mgcv',
21-
'nlme', 'nnet', 'rpart', 'spatial', 'survival'
22-
]);
23-
2411
/** how a declared library relates to the package database */
2512
type MatchStatus = 'matched' | 'base' | 'unmatched' | 'db-unavailable';
2613

@@ -34,12 +21,12 @@ interface LibraryMatch {
3421
type PackageLookup = Pick<PkgDatabase, 'lookup'>;
3522

3623
/**
37-
* Classifies a declared library against the package database: base/recommended packages (which ship with R
38-
* and are never in the CRAN database) are recognised up front; otherwise we report whether the database
39-
* knows the package, or that the database itself was unavailable.
24+
* Classifies a declared library against the package database: base packages (part of R itself, never in the
25+
* CRAN database) are recognised up front; otherwise we report whether the database knows the package, or that
26+
* the database itself was unavailable.
4027
*/
4128
export function classifyLibrary(name: string, db: PackageLookup | undefined): LibraryMatch {
42-
if(BaseAndRecommendedPackages.has(name)) {
29+
if(baseRPackages.has(name)) {
4330
return { status: 'base' };
4431
}
4532
if(!db) {
@@ -59,12 +46,16 @@ interface DeclaredLibrary {
5946

6047
interface ProjectManifest {
6148
/** absolute path of the manifest file */
62-
uri: vscode.Uri;
49+
uri: vscode.Uri;
6350
/** short, human-readable label (relative to the workspace folder) */
64-
label: string;
51+
label: string;
6552
/** e.g. `renv`, `DESCRIPTION`, `rv` */
66-
kind: string;
67-
libraries: DeclaredLibrary[];
53+
kind: string;
54+
/** the package this manifest itself describes (from a `DESCRIPTION`'s `Package:` field), if any */
55+
packageName?: string;
56+
/** the version this manifest declares for itself (from a `DESCRIPTION`'s `Version:` field), if any */
57+
packageVersion?: string;
58+
libraries: DeclaredLibrary[];
6859
}
6960

7061
/** the tree is two levels deep: manifests at the top, their libraries below */
@@ -92,7 +83,7 @@ const statusPresentations: Record<MatchStatus, StatusPresentation> = {
9283
base: {
9384
icon: 'library',
9485
badge: () => 'bundled with R',
95-
tooltip: node => `\`${node.library.name}\` is a base/recommended package that ships with R.`
86+
tooltip: node => `\`${node.library.name}\` is a base package that is part of R itself.`
9687
},
9788
unmatched: {
9889
icon: 'circle-slash',
@@ -113,31 +104,47 @@ const statusPresentations: Record<MatchStatus, StatusPresentation> = {
113104
*/
114105
export function registerProjectView(output: vscode.OutputChannel): { dispose: () => void } {
115106
const data = new FlowrProjectTreeView(output);
116-
const tv = vscode.window.createTreeView(FlowrProjectViewId, { treeDataProvider: data });
117-
data.setTreeView(tv);
107+
const disposables: vscode.Disposable[] = [];
108+
let treeView: vscode.TreeView<ProjectNode> | undefined;
118109

119110
// re-scan when project manifests appear/change/disappear or the workspace layout changes
120111
const watcher = vscode.workspace.createFileSystemWatcher('**/{renv.lock,DESCRIPTION,rv.lock,rproject.toml}');
121112
const refresh = () => void data.refresh();
122113
watcher.onDidCreate(refresh);
123114
watcher.onDidChange(refresh);
124115
watcher.onDidDelete(refresh);
125-
const disposeFolders = vscode.workspace.onDidChangeWorkspaceFolders(refresh);
126-
const disposeConfig = vscode.workspace.onDidChangeConfiguration(e => {
127-
if(e.affectsConfiguration(Settings.Category)) {
128-
refresh();
116+
disposables.push(
117+
watcher,
118+
vscode.workspace.onDidChangeWorkspaceFolders(refresh),
119+
vscode.workspace.onDidChangeConfiguration(e => {
120+
if(e.affectsConfiguration(Settings.Category)) {
121+
refresh();
122+
}
123+
})
124+
);
125+
126+
// The view is contributed behind a `when: vscode-flowr:hasProject` clause so it stays hidden until a manifest
127+
// is found. Some editors (e.g. Positron) do not register a `when`-hidden view yet, so creating the tree view
128+
// while the context is unset errors with "No view is registered". We therefore enable the context first, then
129+
// create the view, then let refresh() set the real state (hiding it again when there is no project).
130+
void (async() => {
131+
await vscode.commands.executeCommand('setContext', ProjectContextKey, true);
132+
try {
133+
treeView = vscode.window.createTreeView(FlowrProjectViewId, { treeDataProvider: data });
134+
data.setTreeView(treeView);
135+
} catch(e) {
136+
output.appendLine(`[Project View] Could not create the tree view: ${(e as Error).message}`);
129137
}
130-
});
131-
132-
void data.refresh();
138+
await data.refresh();
139+
})();
133140

134141
return {
135142
dispose: () => {
136-
tv.dispose();
137-
watcher.dispose();
138-
disposeFolders.dispose();
139-
disposeConfig.dispose();
143+
treeView?.dispose();
140144
data.dispose();
145+
for(const d of disposables) {
146+
d.dispose();
147+
}
141148
}
142149
};
143150
}
@@ -200,11 +207,16 @@ class FlowrProjectTreeView implements vscode.TreeDataProvider<ProjectNode> {
200207
}
201208

202209
function manifestTreeItem(manifest: ProjectManifest): vscode.TreeItem {
203-
const item = new vscode.TreeItem(manifest.label, vscode.TreeItemCollapsibleState.Expanded);
204-
item.description = `${manifest.kind} · ${manifest.libraries.length} librar${manifest.libraries.length === 1 ? 'y' : 'ies'}`;
205-
item.iconPath = new vscode.ThemeIcon('project');
210+
// when the manifest describes a package itself (a DESCRIPTION), surface that package's name and version
211+
const libraryCount = `${manifest.libraries.length} librar${manifest.libraries.length === 1 ? 'y' : 'ies'}`;
212+
const item = new vscode.TreeItem(manifest.packageName ?? manifest.label, vscode.TreeItemCollapsibleState.Expanded);
213+
item.description = [manifest.packageVersion && `v${manifest.packageVersion}`, manifest.kind, libraryCount].filter(Boolean).join(' · ');
214+
item.iconPath = new vscode.ThemeIcon('package');
206215
item.resourceUri = manifest.uri;
207-
item.tooltip = new vscode.MarkdownString(`Detected ${manifest.kind} manifest\n\n\`${manifest.uri.fsPath}\``);
216+
const title = manifest.packageName
217+
? `\`${manifest.packageName}\`${manifest.packageVersion ? ` version ${manifest.packageVersion}` : ''}${manifest.kind}`
218+
: `Detected ${manifest.kind} manifest`;
219+
item.tooltip = new vscode.MarkdownString(`${title}\n\n\`${manifest.uri.fsPath}\``);
208220
item.command = { command: 'vscode.open', title: 'Open manifest', arguments: [manifest.uri] };
209221
return item;
210222
}
@@ -221,16 +233,18 @@ function libraryTreeItem(node: LibraryNode): vscode.TreeItem {
221233

222234
/* ------------------------------------------------------------------ detection ------------------------------------------------------------------ */
223235

224-
/** binds a manifest file name to the project kind it represents and the parser that reads its libraries */
236+
/** binds a manifest file name to the project kind it represents and the parsers that read it */
225237
interface ManifestDetector {
226238
file: string;
227239
kind: string;
228240
parse: (content: string) => DeclaredLibrary[];
241+
/** optional: extract the package this manifest describes itself (name/version) */
242+
meta?: (content: string) => { packageName?: string, packageVersion?: string };
229243
}
230244

231245
const manifestDetectors: readonly ManifestDetector[] = [
232246
{ file: 'renv.lock', kind: 'renv', parse: parseRenvLock },
233-
{ file: 'DESCRIPTION', kind: 'DESCRIPTION', parse: parseDescription },
247+
{ file: 'DESCRIPTION', kind: 'DESCRIPTION', parse: parseDescription, meta: parseDescriptionMeta },
234248
{ file: 'rv.lock', kind: 'rv', parse: parseRvLock },
235249
{ file: 'rproject.toml', kind: 'rv', parse: parseRvToml }
236250
];
@@ -239,7 +253,7 @@ async function detectManifests(output: vscode.OutputChannel): Promise<ProjectMan
239253
const folders = vscode.workspace.workspaceFolders ?? [];
240254
const manifests: ProjectManifest[] = [];
241255
for(const folder of folders) {
242-
for(const { file, kind, parse } of manifestDetectors) {
256+
for(const { file, kind, parse, meta } of manifestDetectors) {
243257
const uri = vscode.Uri.joinPath(folder.uri, file);
244258
let content: string;
245259
try {
@@ -248,7 +262,7 @@ async function detectManifests(output: vscode.OutputChannel): Promise<ProjectMan
248262
continue; // file does not exist
249263
}
250264
try {
251-
manifests.push({ uri, label: file, kind, libraries: dedupeLibraries(parse(content)) });
265+
manifests.push({ uri, label: file, kind, ...meta?.(content), libraries: dedupeLibraries(parse(content)) });
252266
} catch(e) {
253267
output.appendLine(`[Project View] Failed to parse ${uri.fsPath}: ${(e as Error).message}`);
254268
manifests.push({ uri, label: file, kind, libraries: [] });
@@ -284,6 +298,12 @@ export function parseRenvLock(content: string): DeclaredLibrary[] {
284298
}));
285299
}
286300

301+
/** Extracts the package this `DESCRIPTION` describes itself: its `Package:` name and `Version:`. */
302+
export function parseDescriptionMeta(content: string): { packageName?: string, packageVersion?: string } {
303+
const field = (name: string) => new RegExp(`^${name}\\s*:\\s*(.+)$`, 'm').exec(content.replace(/\r\n/g, '\n'))?.[1].trim();
304+
return { packageName: field('Package'), packageVersion: field('Version') };
305+
}
306+
287307
/** Extracts the packages listed in the `Depends`/`Imports`/`Suggests`/`LinkingTo` fields of a `DESCRIPTION` file. */
288308
export function parseDescription(content: string): DeclaredLibrary[] {
289309
// DESCRIPTION is a DCF file; the dependency fields are comma-separated, possibly wrapped across lines

src/package-db.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import { PkgDatabase } from '@eagleoutice/flowr/project/plugins/package-version-
22
import { getBundledPackageDbPath } from './extension';
33
import { getConfig, Settings } from './settings';
44

5+
/**
6+
* R's base priority packages: part of R itself, available without an explicit `library()`, and never
7+
* published on CRAN (so they never appear in the package database).
8+
*/
9+
export const baseRPackages = new Set([
10+
'base', 'compiler', 'datasets', 'grDevices', 'graphics', 'grid', 'methods', 'parallel',
11+
'splines', 'stats', 'stats4', 'tcltk', 'tools', 'translations', 'utils'
12+
]);
13+
514
/** cached database, keyed by the file path it was loaded from */
615
let cache: { path: string, db: PkgDatabase } | undefined;
716

0 commit comments

Comments
 (0)