Skip to content

Exclude section headers from workspace symbols in R packages #755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: feature/lsp-logger
Choose a base branch
from
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
14 changes: 12 additions & 2 deletions apps/lsp/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export interface Settings {
readonly scale: number;
readonly extensions: MathjaxSupportedExtension[];
}
readonly symbols: {
readonly exportToWorkspace: 'default' | 'all' | 'none';
};
};
readonly markdown: {
readonly preferredMdPathExtensionStyle: 'auto' | 'includeExtension' | 'removeExtension';
Expand Down Expand Up @@ -88,6 +91,9 @@ function defaultSettings(): Settings {
mathjax: {
scale: 1,
extensions: []
},
symbols: {
exportToWorkspace: 'all'
}
},
markdown: {
Expand Down Expand Up @@ -165,6 +171,9 @@ export class ConfigurationManager extends Disposable {
mathjax: {
scale: settings.quarto.mathjax.scale,
extensions: settings.quarto.mathjax.extensions
},
symbols: {
exportToWorkspace: settings.quarto.symbols.exportToWorkspace
}
}
};
Expand Down Expand Up @@ -224,12 +233,13 @@ export function lsConfiguration(configManager: ConfigurationManager): LsConfigur
},
get mathjaxExtensions(): readonly MathjaxSupportedExtension[] {
return configManager.getSettings().quarto.mathjax.extensions;
},
get exportSymbolsToWorkspace(): 'default' | 'all' | 'none' {
return configManager.getSettings().quarto.symbols.exportToWorkspace;
}
}
}



export function getDiagnosticsOptions(configManager: ConfigurationManager): DiagnosticOptions {
const settings = configManager.getSettings();
if (!settings) {
Expand Down
27 changes: 27 additions & 0 deletions apps/lsp/src/r-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* r-utils.ts
*
* Copyright (C) 2025 by Posit Software, PBC
*
* Unless you have received this program directly from Posit Software pursuant
* to the terms of a commercial license agreement with Posit Software, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/

import { isRPackage as isRPackageImpl } from "@utils/r-utils";
import { IWorkspace } from './service';

// Version that selects workspace folder
export async function isRPackage(workspace: IWorkspace): Promise<boolean> {
if (workspace.workspaceFolders === undefined) {
return false;
}

const folderUri = workspace.workspaceFolders[0];
return isRPackageImpl(folderUri);
}
4 changes: 3 additions & 1 deletion apps/lsp/src/service/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export interface LsConfiguration {
readonly colorTheme: 'light' | 'dark';
readonly mathjaxScale: number;
readonly mathjaxExtensions: readonly MathjaxSupportedExtension[];
readonly exportSymbolsToWorkspace: 'default' | 'all' | 'none';
}

export const defaultMarkdownFileExtension = 'qmd';
Expand Down Expand Up @@ -109,7 +110,8 @@ const defaultConfig: LsConfiguration = {
includeWorkspaceHeaderCompletions: 'never',
colorTheme: 'light',
mathjaxScale: 1,
mathjaxExtensions: []
mathjaxExtensions: [],
exportSymbolsToWorkspace: 'all'
};

export function defaultLsConfiguration(): LsConfiguration {
Expand Down
2 changes: 1 addition & 1 deletion apps/lsp/src/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export function createLanguageService(init: LanguageServiceInitialization): IMdL
const diagnosticOnSaveComputer = new DiagnosticOnSaveComputer(init.quarto);
const diagnosticsComputer = new DiagnosticComputer(config, init.workspace, linkProvider, tocProvider, logger);
const docSymbolProvider = new MdDocumentSymbolProvider(tocProvider, linkProvider, logger);
const workspaceSymbolProvider = new MdWorkspaceSymbolProvider(init.workspace, docSymbolProvider);
const workspaceSymbolProvider = new MdWorkspaceSymbolProvider(init.workspace, init.config, docSymbolProvider);
const documentHighlightProvider = new MdDocumentHighlightProvider(config, tocProvider, linkProvider);

return Object.freeze<IMdLanguageService>({
Expand Down
19 changes: 18 additions & 1 deletion apps/lsp/src/service/providers/workspace-symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,24 @@ import { Document } from 'quarto-core';
import { IWorkspace } from '../workspace';
import { MdWorkspaceInfoCache } from '../workspace-cache';
import { MdDocumentSymbolProvider } from './document-symbols';
import { LsConfiguration } from '../config';
import { isRPackage } from '../../r-utils';

export class MdWorkspaceSymbolProvider extends Disposable {

readonly #config: LsConfiguration;
readonly #cache: MdWorkspaceInfoCache<readonly lsp.SymbolInformation[]>;
readonly #symbolProvider: MdDocumentSymbolProvider;
readonly #workspace: IWorkspace;

constructor(
workspace: IWorkspace,
config: LsConfiguration,
symbolProvider: MdDocumentSymbolProvider,
) {
super();

this.#workspace = workspace;
this.#config = config;
this.#symbolProvider = symbolProvider;

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

switch (this.#config.exportSymbolsToWorkspace) {
case 'all': break;
case 'default': if (await shouldExportSymbolsToWorkspace(this.#workspace)) return []; else break;
case 'none': return [];
}

const allSymbols = await this.#cache.values();

if (token.isCancellationRequested) {
Expand Down Expand Up @@ -73,3 +86,7 @@ export class MdWorkspaceSymbolProvider extends Disposable {
}
}
}

async function shouldExportSymbolsToWorkspace(workspace: IWorkspace): Promise<boolean> {
return await isRPackage(workspace);
}
4 changes: 3 additions & 1 deletion apps/lsp/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
"lib": ["ES2020"],
"module": "CommonJS",
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true,
"resolveJsonModule": true,
"paths": {
"@utils/*": ["../utils/*"]
}
},
"exclude": ["node_modules"],
"extends": "tsconfig/base.json",
Expand Down
63 changes: 63 additions & 0 deletions apps/utils/r-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* r-utils.ts
*
* Copyright (C) 2025 by Posit Software, PBC
*
* Unless you have received this program directly from Posit Software pursuant
* to the terms of a commercial license agreement with Posit Software, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/

import * as fs from "fs/promises";
import * as path from "path";
import { URI } from 'vscode-uri';

/**
* Checks if the given folder contains an R package.
*
* Determined by:
* - Presence of a `DESCRIPTION` file.
* - Presence of `Package:` field.
* - Presence of `Type: package` field and value.
*
* The fields are checked to disambiguate real packages from book repositories using a `DESCRIPTION` file.
*
* @param folderPath Folder to check for a `DESCRIPTION` file.
*/
export async function isRPackage(folderUri: URI): Promise<boolean> {
// We don't currently support non-file schemes
if (folderUri.scheme !== 'file') {
return false;
}

const descriptionLines = await parseRPackageDescription(folderUri.fsPath);
if (!descriptionLines) {
return false;
}

const packageLines = descriptionLines.filter(line => line.startsWith('Package:'));
const typeLines = descriptionLines.filter(line => line.startsWith('Type:'));

const typeIsPackage = (typeLines.length > 0
? typeLines[0].toLowerCase().includes('package')
: false);
const typeIsPackageOrMissing = typeLines.length === 0 || typeIsPackage;

return packageLines.length > 0 && typeIsPackageOrMissing;
}

async function parseRPackageDescription(folderPath: string): Promise<string[]> {
const filePath = path.join(folderPath, 'DESCRIPTION');

try {
const descriptionText = await fs.readFile(filePath, 'utf8');
return descriptionText.split(/\r?\n/);
} catch {
return [''];
}
}
11 changes: 11 additions & 0 deletions apps/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,17 @@
"error"
],
"markdownDescription": "Log level for the Quarto language server."
},
"quarto.symbols.exportToWorkspace": {
"type": "string",
"enum": ["default", "all", "none"],
"enumDescriptions": [
"Depends on the project type: `\"none\"` in R packages, `\"all\"` otherwise.",
"",
""
],
"default": "default",
"description": "Whether Markdown elements like section headers are included in workspace symbol search."
}
}
},
Expand Down
1 change: 1 addition & 0 deletions apps/vscode/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
*/

import * as fs from "fs";
import * as path from "path";
import {
ExtensionContext,
Expand Down
28 changes: 0 additions & 28 deletions apps/vscode/src/providers/preview/preview-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,34 +60,6 @@ export function isQuartoShinyKnitrDoc(

}

export async function isRPackage(): Promise<boolean> {
const descriptionLines = await parseRPackageDescription();
if (!descriptionLines) {
return false;
}
const packageLines = descriptionLines.filter(line => line.startsWith('Package:'));
const typeLines = descriptionLines.filter(line => line.startsWith('Type:'));
const typeIsPackage = (typeLines.length > 0
? typeLines[0].toLowerCase().includes('package')
: false);
const typeIsPackageOrMissing = typeLines.length === 0 || typeIsPackage;
return packageLines.length > 0 && typeIsPackageOrMissing;
}

async function parseRPackageDescription(): Promise<string[]> {
if (vscode.workspace.workspaceFolders !== undefined) {
const folderUri = vscode.workspace.workspaceFolders[0].uri;
const fileUri = vscode.Uri.joinPath(folderUri, 'DESCRIPTION');
try {
const bytes = await vscode.workspace.fs.readFile(fileUri);
const descriptionText = Buffer.from(bytes).toString('utf8');
const descriptionLines = descriptionText.split(/(\r?\n)/);
return descriptionLines;
} catch { }
}
return [''];
}

export async function renderOnSave(engine: MarkdownEngine, document: TextDocument) {
// if its a notebook and we don't have a save hook for notebooks then don't
// allow renderOnSave (b/c we can't detect the saves)
Expand Down
2 changes: 1 addition & 1 deletion apps/vscode/src/providers/preview/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ import {
haveNotebookSaveEvents,
isQuartoShinyDoc,
isQuartoShinyKnitrDoc,
isRPackage,
renderOnSave,
} from "./preview-util";

Expand All @@ -88,6 +87,7 @@ import {
yamlErrorLocation,
} from "./preview-errors";
import { ExtensionHost } from "../../host";
import { isRPackage } from "../../r-utils";

tmp.setGracefulCleanup();

Expand Down
29 changes: 29 additions & 0 deletions apps/vscode/src/r-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* r-utils.ts
*
* Copyright (C) 2025 by Posit Software, PBC
*
* Unless you have received this program directly from Posit Software pursuant
* to the terms of a commercial license agreement with Posit Software, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/

import * as vscode from 'vscode';
import { isRPackage as isRPackageImpl } from "@utils/r-utils";

// Version that selects workspace folder
export async function isRPackage(): Promise<boolean> {
if (vscode.workspace.workspaceFolders === undefined) {
return false;
}

// Pick first workspace
const workspaceFolder = vscode.workspace.workspaceFolders[0];

return isRPackageImpl(workspaceFolder.uri);
}
6 changes: 5 additions & 1 deletion apps/vscode/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
"outDir": "out",
"lib": ["ES2021"],
"sourceMap": true,
"strict": true /* enable all strict type-checking options */
"strict": true,
"paths": {
"@utils/*": ["../utils/*"]
}
/* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
Expand Down