Skip to content
Merged
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
2 changes: 2 additions & 0 deletions apps/vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<https://github.com/quarto-dev/quarto/pull/875>).
- Added a public API that other extensions can query to get the Quarto CLI path, version, and availability (<https://github.com/quarto-dev/quarto/pull/879>).


## 1.127.0 (Release on 2025-12-17)

Expand Down
95 changes: 95 additions & 0 deletions apps/vscode/src/api.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined> {
* 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;
},
};
}
6 changes: 5 additions & 1 deletion apps/vscode/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<QuartoExtensionApi> {
// create output channel for extension logs and lsp client logs
const outputChannel = vscode.window.createOutputChannel("Quarto", { log: true });

Expand Down Expand Up @@ -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);
}

/**
Expand Down
49 changes: 49 additions & 0 deletions apps/vscode/src/test/api.test.ts
Original file line number Diff line number Diff line change
@@ -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");
}
});
});