Skip to content

Commit ac6e441

Browse files
committed
Use common implementation for R package detection
1 parent 89b9989 commit ac6e441

File tree

9 files changed

+132
-47
lines changed

9 files changed

+132
-47
lines changed

apps/lsp/src/r-utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* r-utils.ts
3+
*
4+
* Copyright (C) 2025 by Posit Software, PBC
5+
*
6+
* Unless you have received this program directly from Posit Software pursuant
7+
* to the terms of a commercial license agreement with Posit Software, then
8+
* this program is licensed to you under the terms of version 3 of the
9+
* GNU Affero General Public License. This program is distributed WITHOUT
10+
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11+
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12+
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13+
*
14+
*/
15+
16+
import { isRPackage as isRPackageImpl } from "@utils/r-utils";
17+
import { IWorkspace } from './service';
18+
19+
// Version that selects workspace folder
20+
export async function isRPackage(workspace: IWorkspace): Promise<boolean> {
21+
if (workspace.workspaceFolders === undefined) {
22+
return false;
23+
}
24+
25+
const folderUri = workspace.workspaceFolders[0];
26+
return isRPackageImpl(folderUri);
27+
}

apps/lsp/src/service/providers/workspace-symbols.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
*
1515
*/
1616

17-
import * as fs from 'fs';
18-
import { Utils } from 'vscode-uri';
1917
import { CancellationToken } from 'vscode-languageserver';
2018
import * as lsp from 'vscode-languageserver-types';
2119
import { Disposable } from 'core';
@@ -24,6 +22,7 @@ import { IWorkspace } from '../workspace';
2422
import { MdWorkspaceInfoCache } from '../workspace-cache';
2523
import { MdDocumentSymbolProvider } from './document-symbols';
2624
import { LsConfiguration } from '../config';
25+
import { isRPackage } from '../../r-utils';
2726

2827
export class MdWorkspaceSymbolProvider extends Disposable {
2928
readonly #config: LsConfiguration;
@@ -52,7 +51,7 @@ export class MdWorkspaceSymbolProvider extends Disposable {
5251

5352
switch (this.#config.exportSymbolsToWorkspace) {
5453
case 'all': break;
55-
case 'default': if (shouldExportSymbolsToWorkspace(this.#workspace)) return []; else break;
54+
case 'default': if (await shouldExportSymbolsToWorkspace(this.#workspace)) return []; else break;
5655
case 'none': return [];
5756
}
5857

@@ -88,16 +87,6 @@ export class MdWorkspaceSymbolProvider extends Disposable {
8887
}
8988
}
9089

91-
function shouldExportSymbolsToWorkspace(workspace: IWorkspace): boolean {
92-
return isRPackage(workspace);
93-
}
94-
95-
function isRPackage(workspace: IWorkspace): boolean {
96-
if (workspace.workspaceFolders === undefined) {
97-
return false;
98-
}
99-
100-
const projectPath = workspace.workspaceFolders[0];
101-
const descPath = Utils.joinPath(projectPath, 'DESCRIPTION');
102-
return fs.existsSync(descPath.fsPath);
90+
async function shouldExportSymbolsToWorkspace(workspace: IWorkspace): Promise<boolean> {
91+
return await isRPackage(workspace);
10392
}

apps/lsp/tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
"lib": ["ES2020"],
44
"module": "CommonJS",
55
"outDir": "./dist",
6-
"rootDir": "./src",
76
"sourceMap": true,
87
"resolveJsonModule": true,
8+
"paths": {
9+
"@utils/*": ["../utils/*"]
10+
}
911
},
1012
"exclude": ["node_modules"],
1113
"extends": "tsconfig/base.json",

apps/utils/r-utils.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* r-utils.ts
3+
*
4+
* Copyright (C) 2025 by Posit Software, PBC
5+
*
6+
* Unless you have received this program directly from Posit Software pursuant
7+
* to the terms of a commercial license agreement with Posit Software, then
8+
* this program is licensed to you under the terms of version 3 of the
9+
* GNU Affero General Public License. This program is distributed WITHOUT
10+
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11+
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12+
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13+
*
14+
*/
15+
16+
import * as fs from "fs/promises";
17+
import * as path from "path";
18+
import { URI } from 'vscode-uri';
19+
20+
/**
21+
* Checks if the given folder contains an R package.
22+
*
23+
* Determined by:
24+
* - Presence of a `DESCRIPTION` file.
25+
* - Presence of `Package:` field.
26+
* - Presence of `Type: package` field and value.
27+
*
28+
* The fields are checked to disambiguate real packages from book repositories using a `DESCRIPTION` file.
29+
*
30+
* @param folderPath Folder to check for a `DESCRIPTION` file.
31+
*/
32+
export async function isRPackage(folderUri: URI): Promise<boolean> {
33+
// We don't currently support non-file schemes
34+
if (folderUri.scheme !== 'file') {
35+
return false;
36+
}
37+
38+
const descriptionLines = await parseRPackageDescription(folderUri.fsPath);
39+
if (!descriptionLines) {
40+
return false;
41+
}
42+
43+
const packageLines = descriptionLines.filter(line => line.startsWith('Package:'));
44+
const typeLines = descriptionLines.filter(line => line.startsWith('Type:'));
45+
46+
const typeIsPackage = (typeLines.length > 0
47+
? typeLines[0].toLowerCase().includes('package')
48+
: false);
49+
const typeIsPackageOrMissing = typeLines.length === 0 || typeIsPackage;
50+
51+
return packageLines.length > 0 && typeIsPackageOrMissing;
52+
}
53+
54+
async function parseRPackageDescription(folderPath: string): Promise<string[]> {
55+
const filePath = path.join(folderPath, 'DESCRIPTION');
56+
57+
try {
58+
const descriptionText = await fs.readFile(filePath, 'utf8');
59+
return descriptionText.split(/\r?\n/);
60+
} catch {
61+
return [''];
62+
}
63+
}

apps/vscode/src/lsp/client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
*
1414
*/
1515

16-
import * as fs from "fs";
1716
import * as path from "path";
1817
import {
1918
ExtensionContext,

apps/vscode/src/providers/preview/preview-util.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -60,34 +60,6 @@ export function isQuartoShinyKnitrDoc(
6060

6161
}
6262

63-
export async function isRPackage(): Promise<boolean> {
64-
const descriptionLines = await parseRPackageDescription();
65-
if (!descriptionLines) {
66-
return false;
67-
}
68-
const packageLines = descriptionLines.filter(line => line.startsWith('Package:'));
69-
const typeLines = descriptionLines.filter(line => line.startsWith('Type:'));
70-
const typeIsPackage = (typeLines.length > 0
71-
? typeLines[0].toLowerCase().includes('package')
72-
: false);
73-
const typeIsPackageOrMissing = typeLines.length === 0 || typeIsPackage;
74-
return packageLines.length > 0 && typeIsPackageOrMissing;
75-
}
76-
77-
async function parseRPackageDescription(): Promise<string[]> {
78-
if (vscode.workspace.workspaceFolders !== undefined) {
79-
const folderUri = vscode.workspace.workspaceFolders[0].uri;
80-
const fileUri = vscode.Uri.joinPath(folderUri, 'DESCRIPTION');
81-
try {
82-
const bytes = await vscode.workspace.fs.readFile(fileUri);
83-
const descriptionText = Buffer.from(bytes).toString('utf8');
84-
const descriptionLines = descriptionText.split(/(\r?\n)/);
85-
return descriptionLines;
86-
} catch { }
87-
}
88-
return [''];
89-
}
90-
9163
export async function renderOnSave(engine: MarkdownEngine, document: TextDocument) {
9264
// if its a notebook and we don't have a save hook for notebooks then don't
9365
// allow renderOnSave (b/c we can't detect the saves)

apps/vscode/src/providers/preview/preview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ import {
7474
haveNotebookSaveEvents,
7575
isQuartoShinyDoc,
7676
isQuartoShinyKnitrDoc,
77-
isRPackage,
7877
renderOnSave,
7978
} from "./preview-util";
8079

@@ -88,6 +87,7 @@ import {
8887
yamlErrorLocation,
8988
} from "./preview-errors";
9089
import { ExtensionHost } from "../../host";
90+
import { isRPackage } from "../../r-utils";
9191

9292
tmp.setGracefulCleanup();
9393

apps/vscode/src/r-utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* r-utils.ts
3+
*
4+
* Copyright (C) 2025 by Posit Software, PBC
5+
*
6+
* Unless you have received this program directly from Posit Software pursuant
7+
* to the terms of a commercial license agreement with Posit Software, then
8+
* this program is licensed to you under the terms of version 3 of the
9+
* GNU Affero General Public License. This program is distributed WITHOUT
10+
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11+
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12+
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13+
*
14+
*/
15+
16+
import * as vscode from 'vscode';
17+
import { isRPackage as isRPackageImpl } from "@utils/r-utils";
18+
19+
// Version that selects workspace folder
20+
export async function isRPackage(): Promise<boolean> {
21+
if (vscode.workspace.workspaceFolders === undefined) {
22+
return false;
23+
}
24+
25+
// Pick first workspace
26+
const workspaceFolder = vscode.workspace.workspaceFolders[0];
27+
28+
return isRPackageImpl(workspaceFolder.uri);
29+
}

apps/vscode/tsconfig.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
"outDir": "out",
66
"lib": ["ES2021"],
77
"sourceMap": true,
8-
"strict": true /* enable all strict type-checking options */
8+
"strict": true,
9+
"paths": {
10+
"@utils/*": ["../utils/*"]
11+
}
12+
/* enable all strict type-checking options */
913
/* Additional Checks */
1014
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
1115
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */

0 commit comments

Comments
 (0)