diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index 0bf87e38..c80b602b 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -3,6 +3,8 @@ ## 1.128.0 (Unreleased) - Fixed a bug where code blocks inside complex div structures (e.g., many `::: {.notes}` divs without preceding blank lines) were not detected as executable cells (). +- Added a public API that other extensions can query to get the Quarto CLI path, version, and availability (). + ## 1.127.0 (Release on 2025-12-17) diff --git a/apps/vscode/src/api.ts b/apps/vscode/src/api.ts new file mode 100644 index 00000000..d033f77a --- /dev/null +++ b/apps/vscode/src/api.ts @@ -0,0 +1,95 @@ +/* + * api.ts + * + * Copyright (C) 2026 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 { QuartoContext } from "quarto-core"; + +/** + * Public API for the Quarto extension. + * + * Other extensions can access this API to get information about the Quarto CLI + * that the Quarto extension is using. This is useful when you need to know + * the exact Quarto binary path, including when Quarto is bundled in Positron + * or installed in a Python virtual environment. + * + * ## Usage from another extension + * + * Since your extension cannot import types from the Quarto extension directly, + * copy this interface definition into your own codebase: + * + * ```typescript + * // Copy this interface into your extension + * interface QuartoExtensionApi { + * getQuartoPath(): string | undefined; + * getQuartoVersion(): string | undefined; + * isQuartoAvailable(): boolean; + * } + * + * // Then use it like this: + * async function getQuartoPathFromExtension(): Promise { + * const quartoExt = vscode.extensions.getExtension('quarto.quarto'); + * if (!quartoExt) { + * return undefined; + * } + * if (!quartoExt.isActive) { + * await quartoExt.activate(); + * } + * const api = quartoExt.exports as QuartoExtensionApi; + * return api.getQuartoPath(); + * } + * ``` + */ +export interface QuartoExtensionApi { + /** + * Get the path to the Quarto CLI binary that the extension is using. + * Returns undefined if Quarto is not available. + */ + getQuartoPath(): string | undefined; + + /** + * Get the version of Quarto that the extension is using. + * Returns undefined if Quarto is not available. + */ + getQuartoVersion(): string | undefined; + + /** + * Check if Quarto is available. + */ + isQuartoAvailable(): boolean; +} + +/** + * Create the public API for the Quarto extension. + */ +export function createQuartoExtensionApi(quartoContext: QuartoContext): QuartoExtensionApi { + return { + getQuartoPath(): string | undefined { + if (!quartoContext.available) { + return undefined; + } + return quartoContext.binPath; + }, + + getQuartoVersion(): string | undefined { + if (!quartoContext.available) { + return undefined; + } + return quartoContext.version; + }, + + isQuartoAvailable(): boolean { + return quartoContext.available; + }, + }; +} diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 7150a3ee..062bcb94 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -44,11 +44,12 @@ import { activateOptionEnterProvider } from "./providers/option"; import { activateBackgroundHighlighter } from "./providers/background"; import { activateContextKeySetter } from "./providers/context-keys"; import { CommandManager } from "./core/command"; +import { createQuartoExtensionApi, QuartoExtensionApi } from "./api"; /** * Entry point for the entire extension! This initializes the LSP, quartoContext, extension host, and more... */ -export async function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext): Promise { // create output channel for extension logs and lsp client logs const outputChannel = vscode.window.createOutputChannel("Quarto", { log: true }); @@ -168,6 +169,9 @@ export async function activate(context: vscode.ExtensionContext) { registerQuartoPathConfigListener(context, outputChannel); outputChannel.info("Activated Quarto extension."); + + // Return the public API for other extensions to use + return createQuartoExtensionApi(quartoContext); } /** diff --git a/apps/vscode/src/test/api.test.ts b/apps/vscode/src/test/api.test.ts new file mode 100644 index 00000000..e26f7cc0 --- /dev/null +++ b/apps/vscode/src/test/api.test.ts @@ -0,0 +1,49 @@ +import * as assert from "assert"; +import { extension } from "./extension"; +import { QuartoExtensionApi } from "../api"; + +suite("Quarto Extension API", function () { + test("Extension exports the API", async function () { + const ext = extension(); + + if (!ext.isActive) { + await ext.activate(); + } + + const api = ext.exports as QuartoExtensionApi; + + assert.ok(api, "Extension should export an API"); + assert.strictEqual(typeof api.getQuartoPath, "function", "API should have getQuartoPath method"); + assert.strictEqual(typeof api.getQuartoVersion, "function", "API should have getQuartoVersion method"); + assert.strictEqual(typeof api.isQuartoAvailable, "function", "API should have isQuartoAvailable method"); + }); + + test("API methods return expected types", async function () { + const ext = extension(); + + if (!ext.isActive) { + await ext.activate(); + } + + const api = ext.exports as QuartoExtensionApi; + + const isAvailable = api.isQuartoAvailable(); + assert.strictEqual(typeof isAvailable, "boolean", "isQuartoAvailable should return a boolean"); + + const path = api.getQuartoPath(); + if (isAvailable) { + assert.strictEqual(typeof path, "string", "getQuartoPath should return a string when Quarto is available"); + assert.ok(path!.length > 0, "getQuartoPath should return a non-empty string"); + } else { + assert.strictEqual(path, undefined, "getQuartoPath should return undefined when Quarto is not available"); + } + + const version = api.getQuartoVersion(); + if (isAvailable) { + assert.strictEqual(typeof version, "string", "getQuartoVersion should return a string when Quarto is available"); + assert.ok(version!.length > 0, "getQuartoVersion should return a non-empty string"); + } else { + assert.strictEqual(version, undefined, "getQuartoVersion should return undefined when Quarto is not available"); + } + }); +});