Skip to content

Commit 605bae6

Browse files
authored
Exclude section headers from workspace symbols in R packages (#858)
1 parent 2ac13a8 commit 605bae6

File tree

26 files changed

+1358
-1163
lines changed

26 files changed

+1358
-1163
lines changed

apps/lsp/src/config.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export interface Settings {
4343
readonly scale: number;
4444
readonly extensions: MathjaxSupportedExtension[];
4545
}
46+
readonly symbols: {
47+
readonly exportToWorkspace: 'default' | 'all' | 'none';
48+
};
4649
};
4750
readonly markdown: {
4851
readonly preferredMdPathExtensionStyle: 'auto' | 'includeExtension' | 'removeExtension';
@@ -88,6 +91,9 @@ function defaultSettings(): Settings {
8891
mathjax: {
8992
scale: 1,
9093
extensions: []
94+
},
95+
symbols: {
96+
exportToWorkspace: 'all'
9197
}
9298
},
9399
markdown: {
@@ -165,6 +171,9 @@ export class ConfigurationManager extends Disposable {
165171
mathjax: {
166172
scale: settings.quarto.mathjax.scale,
167173
extensions: settings.quarto.mathjax.extensions
174+
},
175+
symbols: {
176+
exportToWorkspace: settings.quarto.symbols.exportToWorkspace
168177
}
169178
}
170179
};
@@ -225,12 +234,13 @@ export function lsConfiguration(configManager: ConfigurationManager): LsConfigur
225234
},
226235
get mathjaxExtensions(): readonly MathjaxSupportedExtension[] {
227236
return configManager.getSettings().quarto.mathjax.extensions;
237+
},
238+
get exportSymbolsToWorkspace(): 'default' | 'all' | 'none' {
239+
return configManager.getSettings().quarto.symbols.exportToWorkspace;
228240
}
229241
}
230242
}
231243

232-
233-
234244
export function getDiagnosticsOptions(configManager: ConfigurationManager): DiagnosticOptions {
235245
const settings = configManager.getSettings();
236246
if (!settings) {

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/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export interface LsConfiguration {
7979
readonly colorTheme: 'light' | 'dark';
8080
readonly mathjaxScale: number;
8181
readonly mathjaxExtensions: readonly MathjaxSupportedExtension[];
82+
readonly exportSymbolsToWorkspace: 'default' | 'all' | 'none';
8283
}
8384

8485
export const defaultMarkdownFileExtension = 'qmd';
@@ -109,7 +110,8 @@ const defaultConfig: LsConfiguration = {
109110
includeWorkspaceHeaderCompletions: 'never',
110111
colorTheme: 'light',
111112
mathjaxScale: 1,
112-
mathjaxExtensions: []
113+
mathjaxExtensions: [],
114+
exportSymbolsToWorkspace: 'all'
113115
};
114116

115117
export function defaultLsConfiguration(): LsConfiguration {

apps/lsp/src/service/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ export function createLanguageService(init: LanguageServiceInitialization): IMdL
209209
const diagnosticOnSaveComputer = new DiagnosticOnSaveComputer(init.quarto);
210210
const diagnosticsComputer = new DiagnosticComputer(config, init.workspace, linkProvider, tocProvider, logger);
211211
const docSymbolProvider = new MdDocumentSymbolProvider(tocProvider, linkProvider, logger);
212-
const workspaceSymbolProvider = new MdWorkspaceSymbolProvider(init.workspace, docSymbolProvider);
212+
const workspaceSymbolProvider = new MdWorkspaceSymbolProvider(init.workspace, init.config, docSymbolProvider);
213213
const documentHighlightProvider = new MdDocumentHighlightProvider(config, tocProvider, linkProvider);
214214

215215
return Object.freeze<IMdLanguageService>({

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,24 @@ import { Document } from 'quarto-core';
2121
import { IWorkspace } from '../workspace';
2222
import { MdWorkspaceInfoCache } from '../workspace-cache';
2323
import { MdDocumentSymbolProvider } from './document-symbols';
24+
import { LsConfiguration } from '../config';
25+
import { isRPackage } from '../../r-utils';
2426

2527
export class MdWorkspaceSymbolProvider extends Disposable {
26-
28+
readonly #config: LsConfiguration;
2729
readonly #cache: MdWorkspaceInfoCache<readonly lsp.SymbolInformation[]>;
2830
readonly #symbolProvider: MdDocumentSymbolProvider;
31+
readonly #workspace: IWorkspace;
2932

3033
constructor(
3134
workspace: IWorkspace,
35+
config: LsConfiguration,
3236
symbolProvider: MdDocumentSymbolProvider,
3337
) {
3438
super();
39+
40+
this.#workspace = workspace;
41+
this.#config = config;
3542
this.#symbolProvider = symbolProvider;
3643

3744
this.#cache = this._register(new MdWorkspaceInfoCache(workspace, (doc, token) => this.provideDocumentSymbolInformation(doc, token)));
@@ -42,6 +49,12 @@ export class MdWorkspaceSymbolProvider extends Disposable {
4249
return [];
4350
}
4451

52+
switch (this.#config.exportSymbolsToWorkspace) {
53+
case 'all': break;
54+
case 'default': if (await shouldExportSymbolsToWorkspace(this.#workspace)) return []; else break;
55+
case 'none': return [];
56+
}
57+
4558
const allSymbols = await this.#cache.values();
4659

4760
if (token.isCancellationRequested) {
@@ -73,3 +86,7 @@ export class MdWorkspaceSymbolProvider extends Disposable {
7386
}
7487
}
7588
}
89+
90+
async function shouldExportSymbolsToWorkspace(workspace: IWorkspace): Promise<boolean> {
91+
return await isRPackage(workspace);
92+
}

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/.vscode-test.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,13 @@ export default defineConfig([
88
timeout: 5000,
99
},
1010
},
11+
// R project workspace
12+
{
13+
label: 'r-project',
14+
files: 'test-out/r-project.test.js',
15+
workspaceFolder: 'src/test/examples/r-project',
16+
mocha: {
17+
timeout: 5000,
18+
},
19+
},
1120
]);

apps/vscode/.vscode/launch.json

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@
1313
"outFiles": ["${workspaceFolder}/out/**/*.js"],
1414
"preLaunchTask": "${defaultBuildTask}"
1515
},
16-
{
17-
"type": "node",
18-
"request": "attach",
19-
"name": "Attach to Server",
20-
"port": 6009,
21-
"restart": true,
22-
"outFiles": ["${workspaceRoot}/out/**/*.js"]
23-
}
16+
{
17+
"name": "Extension Tests",
18+
"type": "extensionHost",
19+
"request": "launch",
20+
"runtimeExecutable": "${execPath}",
21+
"args": [
22+
"--extensionDevelopmentPath=${workspaceFolder}",
23+
"--extensionTestsPath=${workspaceFolder}/test-out",
24+
"${workspaceFolder}/src/test/examples"
25+
],
26+
"outFiles": ["${workspaceFolder}/test-out/**/*.js"],
27+
"preLaunchTask": "yarn: build-test"
28+
}
2429
]
2530
}

apps/vscode/.vscode/tasks.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
"kind": "build",
1616
"isDefault": true
1717
}
18+
},
19+
{
20+
"label": "yarn: build-test",
21+
"type": "shell",
22+
"command": "yarn",
23+
"args": ["build-test"],
24+
"problemMatcher": []
1825
}
1926
]
2027
}

0 commit comments

Comments
 (0)