From ef4202a6de192f8db7fb4d279e922d143e823919 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Fri, 2 Jan 2026 15:35:42 -0700 Subject: [PATCH 1/4] Add some logging for Quarto CLI discovery --- apps/vscode/src/core/quarto.ts | 52 +++++++++++-- apps/vscode/src/main.ts | 21 +++++- packages/quarto-core/src/context.ts | 113 ++++++++++++++++++++++------ 3 files changed, 153 insertions(+), 33 deletions(-) diff --git a/apps/vscode/src/core/quarto.ts b/apps/vscode/src/core/quarto.ts index 3f3531db..d000a519 100644 --- a/apps/vscode/src/core/quarto.ts +++ b/apps/vscode/src/core/quarto.ts @@ -18,7 +18,7 @@ import * as fs from "node:fs"; import { window, env, workspace, Uri } from "vscode"; import { tryAcquirePositronApi } from "@posit-dev/positron"; -import { QuartoContext } from "quarto-core"; +import { QuartoContext, QuartoSource } from "quarto-core"; import { activePythonInterpreter, pythonIsCondaEnv, pythonIsVenv } from "./python"; import { isWindows } from "./platform"; @@ -26,21 +26,41 @@ import { isWindows } from "./platform"; import semver from "semver"; -export async function configuredQuartoPath() { +/** + * Result of configuredQuartoPath including the path and source. + */ +export interface ConfiguredQuartoPathResult { + path: string; + source: QuartoSource; +} + +/** + * Searches for Quarto in VS Code-specific locations (settings, Positron bundled, pip/venv). + * + * @param logger Optional logger for verbose output + * @returns The path and source if found, undefined otherwise + */ +export async function configuredQuartoPath( + logger?: (msg: string) => void +): Promise { const config = workspace.getConfiguration("quarto"); // explicitly configured trumps everything const quartoPath = config.get("path") as string | undefined; if (quartoPath) { - return quartoPath; + logger?.(` Checking quarto.path setting: ${quartoPath}`); + return { path: quartoPath, source: "setting" }; + } else { + logger?.(" Checking quarto.path setting: not configured"); } // check if we should use bundled Quarto in Positron const useBundledQuarto = config.get("useBundledQuartoInPositron", false); // Default is now false + const isPositron = tryAcquirePositronApi(); + if (useBundledQuarto) { - // Check if we're in Positron - const isPositron = tryAcquirePositronApi(); + logger?.(" Checking Positron bundled Quarto: enabled"); if (isPositron) { // Use path relative to the application root for Positron's bundled Quarto @@ -53,9 +73,11 @@ export async function configuredQuartoPath() { ); if (fs.existsSync(positronQuartoPath)) { - return positronQuartoPath; + logger?.(` Found Positron bundled Quarto at ${positronQuartoPath}`); + return { path: positronQuartoPath, source: "positron-bundled" }; } else { // Log error when bundled Quarto can't be found + logger?.(` Positron bundled Quarto not found at ${positronQuartoPath}`); console.error( `useBundledQuartoInPositron is enabled but bundled Quarto not found at expected path: ${positronQuartoPath}. ` + `Verify Quarto is bundled in the Positron installation.` @@ -65,12 +87,17 @@ export async function configuredQuartoPath() { "Unable to find bundled Quarto in Positron; falling back to system installation" ); } + } else { + logger?.(" Not running in Positron, skipping bundled Quarto check"); } + } else { + logger?.(` Checking Positron bundled Quarto: disabled (useBundledQuartoInPositron = false)`); } // if we can use pip quarto then look for it within the currently python (if its a venv/condaenv) const usePipQuarto = config.get("usePipQuarto", true); if (usePipQuarto) { + logger?.(" Checking pip-installed Quarto in Python venv/conda..."); const python = await activePythonInterpreter(); if (python) { if (pythonIsVenv(python) || pythonIsCondaEnv(python)) { @@ -78,11 +105,22 @@ export async function configuredQuartoPath() { const binDir = path.dirname(python); const quartoPath = path.join(binDir, isWindows() ? "quarto.exe" : "quarto"); if (fs.existsSync(quartoPath)) { - return quartoPath; + logger?.(` Found pip-installed Quarto at ${quartoPath}`); + return { path: quartoPath, source: "pip-venv" }; + } else { + logger?.(` No Quarto found in Python environment at ${binDir}`); } + } else { + logger?.(" Active Python is not in a venv or conda environment"); } + } else { + logger?.(" No active Python interpreter found"); } + } else { + logger?.(" Checking pip-installed Quarto: disabled (usePipQuarto = false)"); } + + return undefined; } diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 7150a3ee..dba30526 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -32,7 +32,7 @@ import { activateEditor } from "./providers/editor/editor"; import { activateCopyFiles } from "./providers/copyfiles"; import { activateZotero } from "./providers/zotero/zotero"; import { extensionHost } from "./host"; -import { initQuartoContext } from "quarto-core"; +import { initQuartoContext, getSourceDescription } from "quarto-core"; import { configuredQuartoPath } from "./core/quarto"; import { activateDenoConfig } from "./providers/deno-config"; import { textFormattingCommands } from "./providers/text-format"; @@ -64,17 +64,30 @@ export async function activate(context: vscode.ExtensionContext) { const commands = cellCommands(host, engine); // get quarto context (some features conditional on it) - const quartoPath = await configuredQuartoPath(); + // Create a logger function for verbose discovery output + const discoveryLogger = (msg: string) => outputChannel.info(msg); + + outputChannel.info("Searching for Quarto CLI..."); + const quartoPathResult = await configuredQuartoPath(discoveryLogger); const workspaceFolder = vscode.workspace.workspaceFolders?.length ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined; const quartoContext = initQuartoContext( - quartoPath, + quartoPathResult?.path, workspaceFolder, // Look for quarto in the app root; this is where Positron installs it [path.join(vscode.env.appRoot, "quarto", "bin")], - vscode.window.showWarningMessage + vscode.window.showWarningMessage, + { logger: discoveryLogger, source: quartoPathResult?.source } ); + + // Log the final discovery result + if (quartoContext.available) { + const sourceDescription = getSourceDescription(quartoContext.source); + outputChannel.info(`Using Quarto ${quartoContext.version} from ${quartoContext.binPath}${sourceDescription}`); + } else { + outputChannel.info("Quarto CLI not found. Some features will be unavailable."); + } if (quartoContext.available) { // enable commands conditional on quarto installation diff --git a/packages/quarto-core/src/context.ts b/packages/quarto-core/src/context.ts index 878df102..757b2080 100644 --- a/packages/quarto-core/src/context.ts +++ b/packages/quarto-core/src/context.ts @@ -21,6 +21,39 @@ import { ExecFileSyncOptions } from "node:child_process"; import * as semver from "semver"; import { execProgram, isArm_64 } from "core-node"; +/** + * Describes where Quarto was discovered from. + */ +export type QuartoSource = + | "setting" // quarto.path setting + | "positron-bundled" // Positron's bundled Quarto + | "pip-venv" // pip-installed in venv/conda + | "path" // Found on system PATH + | "known-location" // Known install location + | "additional-path"; // Additional search path (Positron fallback) + +/** + * Get a human-readable description of the Quarto source for logging. + */ +export function getSourceDescription(source: QuartoSource | undefined): string { + switch (source) { + case "setting": + return " (configured via quarto.path setting)"; + case "positron-bundled": + return " (Positron bundled)"; + case "pip-venv": + return " (pip-installed in Python environment)"; + case "path": + return " (found on system PATH)"; + case "known-location": + return " (found in known installation location)"; + case "additional-path": + return " (found in additional search path)"; + default: + return ""; + } +} + export interface QuartoContext { available: boolean; version: string; @@ -29,6 +62,7 @@ export interface QuartoContext { pandocPath: string; workspaceDir?: string; useCmd: boolean; + source?: QuartoSource; runQuarto: (options: ExecFileSyncOptions, ...args: string[]) => string; runPandoc: (options: ExecFileSyncOptions, ...args: string[]) => string; } @@ -51,10 +85,13 @@ export function initQuartoContext( quartoPath?: string, workspaceFolder?: string, additionalSearchPaths?: string[], - showWarning?: (msg: string) => void + showWarning?: (msg: string) => void, + options?: { logger?: (msg: string) => void; source?: QuartoSource } ): QuartoContext { // default warning to log showWarning = showWarning || console.log; + const logger = options?.logger; + let source = options?.source; // check for user setting (resolving workspace relative paths) let quartoInstall: QuartoInstallation | undefined; @@ -63,16 +100,32 @@ export function initQuartoContext( quartoPath = path.join(workspaceFolder, quartoPath); } quartoInstall = detectUserSpecifiedQuarto(quartoPath, showWarning); + // If a source wasn't provided and we have a path, assume it's from a setting + if (quartoInstall && !source) { + source = "setting"; + } } // next look on the path if (!quartoInstall) { + logger?.(" Checking system PATH..."); quartoInstall = detectQuarto("quarto"); + if (quartoInstall) { + logger?.(` Found Quarto ${quartoInstall.version} on system PATH`); + source = "path"; + } else { + logger?.(" Not found on system PATH"); + } } // if still not found, scan for versions of quarto in known locations if (!quartoInstall) { - quartoInstall = scanForQuarto(additionalSearchPaths); + logger?.(" Scanning known installation locations..."); + const result = scanForQuartoWithSource(additionalSearchPaths, logger); + quartoInstall = result.install; + if (result.install) { + source = result.source; + } } // return if we got them @@ -96,6 +149,7 @@ export function initQuartoContext( pandocPath, workspaceDir: workspaceFolder, useCmd, + source, runQuarto: (options: ExecFileSyncOptions, ...args: string[]) => execProgram( path.join(quartoInstall!.binPath, "quarto" + (useCmd ? ".cmd" : "")), @@ -110,6 +164,7 @@ export function initQuartoContext( ), }; } else { + logger?.(" Quarto CLI not found"); return quartoContextUnavailable(); } } @@ -190,44 +245,58 @@ function detectUserSpecifiedQuarto( } /** - * Scan for Quarto in known locations. + * Scan for Quarto in known locations, returning the source. * * @param additionalSearchPaths Additional paths to search for Quarto (optional) + * @param logger Optional logger for verbose output * - * @returns A Quarto installation if found, otherwise undefined + * @returns An object containing the installation (if found) and the source */ -function scanForQuarto(additionalSearchPaths?: string[]): QuartoInstallation | undefined { - const scanPaths: string[] = []; +function scanForQuartoWithSource( + additionalSearchPaths?: string[], + logger?: (msg: string) => void +): { install: QuartoInstallation | undefined; source: QuartoSource | undefined } { + const knownPaths: string[] = []; if (os.platform() === "win32") { - scanPaths.push("C:\\Program Files\\Quarto\\bin"); + knownPaths.push("C:\\Program Files\\Quarto\\bin"); const localAppData = process.env["LOCALAPPDATA"]; if (localAppData) { - scanPaths.push(path.join(localAppData, "Programs", "Quarto", "bin")); + knownPaths.push(path.join(localAppData, "Programs", "Quarto", "bin")); } - scanPaths.push("C:\\Program Files\\RStudio\\bin\\quarto\\bin"); + knownPaths.push("C:\\Program Files\\RStudio\\bin\\quarto\\bin"); } else if (os.platform() === "darwin") { - scanPaths.push("/Applications/quarto/bin"); + knownPaths.push("/Applications/quarto/bin"); const home = process.env.HOME; if (home) { - scanPaths.push(path.join(home, "Applications", "quarto", "bin")); + knownPaths.push(path.join(home, "Applications", "quarto", "bin")); } - scanPaths.push("/Applications/RStudio.app/Contents/MacOS/quarto/bin"); + knownPaths.push("/Applications/RStudio.app/Contents/MacOS/quarto/bin"); } else if (os.platform() === "linux") { - scanPaths.push("/opt/quarto/bin"); - scanPaths.push("/usr/lib/rstudio/bin/quarto/bin"); - scanPaths.push("/usr/lib/rstudio-server/bin/quarto/bin"); - } - - if (additionalSearchPaths) { - scanPaths.push(...additionalSearchPaths); + knownPaths.push("/opt/quarto/bin"); + knownPaths.push("/usr/lib/rstudio/bin/quarto/bin"); + knownPaths.push("/usr/lib/rstudio-server/bin/quarto/bin"); } - for (const scanPath of scanPaths.filter(fs.existsSync)) { + // Check known locations first + for (const scanPath of knownPaths.filter(fs.existsSync)) { const install = detectQuarto(path.join(scanPath, "quarto")); if (install) { - return install; + logger?.(` Found Quarto ${install.version} at ${scanPath}`); + return { install, source: "known-location" }; + } + } + + // Then check additional search paths (e.g., Positron bundled location) + if (additionalSearchPaths) { + for (const scanPath of additionalSearchPaths.filter(fs.existsSync)) { + const install = detectQuarto(path.join(scanPath, "quarto")); + if (install) { + logger?.(` Found Quarto ${install.version} at ${scanPath} (additional search path)`); + return { install, source: "additional-path" }; + } } } - return undefined; + logger?.(" Not found in known installation locations"); + return { install: undefined, source: undefined }; } From 08ea1cc57a1a09720e882d20cc305ee9a948145a Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Fri, 2 Jan 2026 15:37:14 -0700 Subject: [PATCH 2/4] Pass resource to `getConfiguration()` to avoid polluting logs --- apps/vscode/src/providers/copyfiles/filename.ts | 2 +- apps/vscode/src/providers/deno-config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/vscode/src/providers/copyfiles/filename.ts b/apps/vscode/src/providers/copyfiles/filename.ts index ffdc9d38..c25b083f 100644 --- a/apps/vscode/src/providers/copyfiles/filename.ts +++ b/apps/vscode/src/providers/copyfiles/filename.ts @@ -41,7 +41,7 @@ export async function getNewFileName(document: vscode.TextDocument, file: vscode function getDesiredNewFilePath(document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri { const docUri = getParentDocumentUri(document); - const config = vscode.workspace.getConfiguration('markdown').get>('experimental.copyFiles.destination') ?? {}; + const config = vscode.workspace.getConfiguration('markdown', docUri).get>('experimental.copyFiles.destination') ?? {}; for (const [rawGlob, rawDest] of Object.entries(config)) { for (const glob of parseGlob(rawGlob)) { if (picomatch.isMatch(docUri.path, glob)) { diff --git a/apps/vscode/src/providers/deno-config.ts b/apps/vscode/src/providers/deno-config.ts index 2fa972df..f5ddcbc2 100644 --- a/apps/vscode/src/providers/deno-config.ts +++ b/apps/vscode/src/providers/deno-config.ts @@ -24,7 +24,7 @@ export function activateDenoConfig(context: ExtensionContext, engine: MarkdownEn if (extensions.getExtension("denoland.vscode-deno")) { const ensureDenoConfig = async (doc: TextDocument) => { if (isQuartoDoc(doc)) { - const config = workspace.getConfiguration(); + const config = workspace.getConfiguration(undefined, doc.uri); const inspectDenoEnable = config.inspect("deno.enable"); if ( !inspectDenoEnable?.globalValue && From 42f6108679a27f0e5bd2f3416d7ea8a54c533205 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Fri, 2 Jan 2026 15:52:04 -0700 Subject: [PATCH 3/4] Don't fetch ALL settings, to address warnings in logs --- apps/lsp/src/config.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index 031e440e..5b235803 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -158,22 +158,27 @@ export class ConfigurationManager extends Disposable { public async update() { this._logger.logTrace('Sending \'configuration\' request'); - const settings = await this.connection_.workspace.getConfiguration(); + // Request only the specific sections we need to avoid warnings about + // language-scoped settings like [markdown], [python], etc. + const [workbench, quarto] = await this.connection_.workspace.getConfiguration([ + { section: 'workbench' }, + { section: 'quarto' } + ]); this._settings = { ...defaultSettings(), workbench: { - colorTheme: settings.workbench.colorTheme + colorTheme: workbench?.colorTheme ?? this._settings.workbench.colorTheme }, quarto: { - logLevel: Logger.parseLogLevel(settings.quarto.server.logLevel), - path: settings.quarto.path, + logLevel: Logger.parseLogLevel(quarto?.server?.logLevel), + path: quarto?.path ?? this._settings.quarto.path, mathjax: { - scale: settings.quarto.mathjax.scale, - extensions: settings.quarto.mathjax.extensions + scale: quarto?.mathjax?.scale ?? this._settings.quarto.mathjax.scale, + extensions: quarto?.mathjax?.extensions ?? this._settings.quarto.mathjax.extensions }, symbols: { - exportToWorkspace: settings.quarto.symbols.exportToWorkspace + exportToWorkspace: quarto?.symbols?.exportToWorkspace ?? this._settings.quarto.symbols.exportToWorkspace } } }; From 35adc1e199fee218dc0fcd6da56f484995dc07b1 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Tue, 6 Jan 2026 13:25:44 -0700 Subject: [PATCH 4/4] Feedback on logs from @vezwork --- apps/vscode/src/core/quarto.ts | 3 +-- packages/quarto-core/src/context.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/vscode/src/core/quarto.ts b/apps/vscode/src/core/quarto.ts index d000a519..b7ecf62c 100644 --- a/apps/vscode/src/core/quarto.ts +++ b/apps/vscode/src/core/quarto.ts @@ -60,9 +60,8 @@ export async function configuredQuartoPath( const isPositron = tryAcquirePositronApi(); if (useBundledQuarto) { - logger?.(" Checking Positron bundled Quarto: enabled"); - if (isPositron) { + logger?.(" Checking Positron bundled Quarto: enabled"); // Use path relative to the application root for Positron's bundled Quarto const rootPath = env.appRoot; // Use vscode.env.appRoot as the application root path const positronQuartoPath = path.join( diff --git a/packages/quarto-core/src/context.ts b/packages/quarto-core/src/context.ts index 757b2080..a27cc37b 100644 --- a/packages/quarto-core/src/context.ts +++ b/packages/quarto-core/src/context.ts @@ -30,7 +30,8 @@ export type QuartoSource = | "pip-venv" // pip-installed in venv/conda | "path" // Found on system PATH | "known-location" // Known install location - | "additional-path"; // Additional search path (Positron fallback) + | "additional-path" // Additional search path (Positron fallback) + | "unknown"; // Source not specified by caller /** * Get a human-readable description of the Quarto source for logging. @@ -49,6 +50,8 @@ export function getSourceDescription(source: QuartoSource | undefined): string { return " (found in known installation location)"; case "additional-path": return " (found in additional search path)"; + case "unknown": + return " (source not specified)"; default: return ""; } @@ -100,9 +103,9 @@ export function initQuartoContext( quartoPath = path.join(workspaceFolder, quartoPath); } quartoInstall = detectUserSpecifiedQuarto(quartoPath, showWarning); - // If a source wasn't provided and we have a path, assume it's from a setting + // If a source wasn't provided and we have a path, mark as unknown if (quartoInstall && !source) { - source = "setting"; + source = "unknown"; } }