Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
19 changes: 12 additions & 7 deletions apps/lsp/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +171 to +181
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, more defensive (less error possibilities and risk of things being undefined) and probably more correct.

}
}
};
Expand Down
52 changes: 45 additions & 7 deletions apps/vscode/src/core/quarto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,49 @@ 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";


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<ConfiguredQuartoPathResult | undefined> {

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
Expand All @@ -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.`
Expand All @@ -65,24 +87,40 @@ 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)) {
// check if there is a quarto in the parent directory
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;
}


Expand Down
21 changes: 17 additions & 4 deletions apps/vscode/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/vscode/src/providers/copyfiles/filename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string>>('experimental.copyFiles.destination') ?? {};
const config = vscode.workspace.getConfiguration('markdown', docUri).get<Record<string, string>>('experimental.copyFiles.destination') ?? {};
for (const [rawGlob, rawDest] of Object.entries(config)) {
for (const glob of parseGlob(rawGlob)) {
if (picomatch.isMatch(docUri.path, glob)) {
Expand Down
2 changes: 1 addition & 1 deletion apps/vscode/src/providers/deno-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
113 changes: 91 additions & 22 deletions packages/quarto-core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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" : "")),
Expand All @@ -110,6 +164,7 @@ export function initQuartoContext(
),
};
} else {
logger?.(" Quarto CLI not found");
return quartoContextUnavailable();
}
}
Expand Down Expand Up @@ -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 };
}