Skip to content

Commit 569fded

Browse files
authored
Add a public API for other extensions to use (#879)
* Create a new API for other extensions to use * Add some tests for new extension API * Update CHANGELOG
1 parent 81147b4 commit 569fded

4 files changed

Lines changed: 151 additions & 1 deletion

File tree

apps/vscode/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## 1.128.0 (Unreleased)
44

55
- 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>).
6+
- 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>).
7+
68

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

apps/vscode/src/api.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* api.ts
3+
*
4+
* Copyright (C) 2026 by Posit Software, PBC
5+
*
6+
* Unless you have received this program directly from Posit Software pursuant
7+
* to the terms of a commercial license agreement with Posit Software, then
8+
* this program is licensed to you under the terms of version 3 of the
9+
* GNU Affero General Public License. This program is distributed WITHOUT
10+
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11+
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12+
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13+
*
14+
*/
15+
16+
import { QuartoContext } from "quarto-core";
17+
18+
/**
19+
* Public API for the Quarto extension.
20+
*
21+
* Other extensions can access this API to get information about the Quarto CLI
22+
* that the Quarto extension is using. This is useful when you need to know
23+
* the exact Quarto binary path, including when Quarto is bundled in Positron
24+
* or installed in a Python virtual environment.
25+
*
26+
* ## Usage from another extension
27+
*
28+
* Since your extension cannot import types from the Quarto extension directly,
29+
* copy this interface definition into your own codebase:
30+
*
31+
* ```typescript
32+
* // Copy this interface into your extension
33+
* interface QuartoExtensionApi {
34+
* getQuartoPath(): string | undefined;
35+
* getQuartoVersion(): string | undefined;
36+
* isQuartoAvailable(): boolean;
37+
* }
38+
*
39+
* // Then use it like this:
40+
* async function getQuartoPathFromExtension(): Promise<string | undefined> {
41+
* const quartoExt = vscode.extensions.getExtension('quarto.quarto');
42+
* if (!quartoExt) {
43+
* return undefined;
44+
* }
45+
* if (!quartoExt.isActive) {
46+
* await quartoExt.activate();
47+
* }
48+
* const api = quartoExt.exports as QuartoExtensionApi;
49+
* return api.getQuartoPath();
50+
* }
51+
* ```
52+
*/
53+
export interface QuartoExtensionApi {
54+
/**
55+
* Get the path to the Quarto CLI binary that the extension is using.
56+
* Returns undefined if Quarto is not available.
57+
*/
58+
getQuartoPath(): string | undefined;
59+
60+
/**
61+
* Get the version of Quarto that the extension is using.
62+
* Returns undefined if Quarto is not available.
63+
*/
64+
getQuartoVersion(): string | undefined;
65+
66+
/**
67+
* Check if Quarto is available.
68+
*/
69+
isQuartoAvailable(): boolean;
70+
}
71+
72+
/**
73+
* Create the public API for the Quarto extension.
74+
*/
75+
export function createQuartoExtensionApi(quartoContext: QuartoContext): QuartoExtensionApi {
76+
return {
77+
getQuartoPath(): string | undefined {
78+
if (!quartoContext.available) {
79+
return undefined;
80+
}
81+
return quartoContext.binPath;
82+
},
83+
84+
getQuartoVersion(): string | undefined {
85+
if (!quartoContext.available) {
86+
return undefined;
87+
}
88+
return quartoContext.version;
89+
},
90+
91+
isQuartoAvailable(): boolean {
92+
return quartoContext.available;
93+
},
94+
};
95+
}

apps/vscode/src/main.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@ import { activateOptionEnterProvider } from "./providers/option";
4444
import { activateBackgroundHighlighter } from "./providers/background";
4545
import { activateContextKeySetter } from "./providers/context-keys";
4646
import { CommandManager } from "./core/command";
47+
import { createQuartoExtensionApi, QuartoExtensionApi } from "./api";
4748

4849
/**
4950
* Entry point for the entire extension! This initializes the LSP, quartoContext, extension host, and more...
5051
*/
51-
export async function activate(context: vscode.ExtensionContext) {
52+
export async function activate(context: vscode.ExtensionContext): Promise<QuartoExtensionApi> {
5253
// create output channel for extension logs and lsp client logs
5354
const outputChannel = vscode.window.createOutputChannel("Quarto", { log: true });
5455

@@ -181,6 +182,9 @@ export async function activate(context: vscode.ExtensionContext) {
181182
registerQuartoPathConfigListener(context, outputChannel);
182183

183184
outputChannel.info("Activated Quarto extension.");
185+
186+
// Return the public API for other extensions to use
187+
return createQuartoExtensionApi(quartoContext);
184188
}
185189

186190
/**

apps/vscode/src/test/api.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as assert from "assert";
2+
import { extension } from "./extension";
3+
import { QuartoExtensionApi } from "../api";
4+
5+
suite("Quarto Extension API", function () {
6+
test("Extension exports the API", async function () {
7+
const ext = extension();
8+
9+
if (!ext.isActive) {
10+
await ext.activate();
11+
}
12+
13+
const api = ext.exports as QuartoExtensionApi;
14+
15+
assert.ok(api, "Extension should export an API");
16+
assert.strictEqual(typeof api.getQuartoPath, "function", "API should have getQuartoPath method");
17+
assert.strictEqual(typeof api.getQuartoVersion, "function", "API should have getQuartoVersion method");
18+
assert.strictEqual(typeof api.isQuartoAvailable, "function", "API should have isQuartoAvailable method");
19+
});
20+
21+
test("API methods return expected types", async function () {
22+
const ext = extension();
23+
24+
if (!ext.isActive) {
25+
await ext.activate();
26+
}
27+
28+
const api = ext.exports as QuartoExtensionApi;
29+
30+
const isAvailable = api.isQuartoAvailable();
31+
assert.strictEqual(typeof isAvailable, "boolean", "isQuartoAvailable should return a boolean");
32+
33+
const path = api.getQuartoPath();
34+
if (isAvailable) {
35+
assert.strictEqual(typeof path, "string", "getQuartoPath should return a string when Quarto is available");
36+
assert.ok(path!.length > 0, "getQuartoPath should return a non-empty string");
37+
} else {
38+
assert.strictEqual(path, undefined, "getQuartoPath should return undefined when Quarto is not available");
39+
}
40+
41+
const version = api.getQuartoVersion();
42+
if (isAvailable) {
43+
assert.strictEqual(typeof version, "string", "getQuartoVersion should return a string when Quarto is available");
44+
assert.ok(version!.length > 0, "getQuartoVersion should return a non-empty string");
45+
} else {
46+
assert.strictEqual(version, undefined, "getQuartoVersion should return undefined when Quarto is not available");
47+
}
48+
});
49+
});

0 commit comments

Comments
 (0)