diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index d8e0c534..9c22c0de 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -4,6 +4,7 @@ - Added clickable document links for file paths in `_quarto.yml` files. File paths are now clickable and navigate directly to the referenced file (). - Added filepath autocompletion in `_quarto.yml` files. When editing YAML values, the extension now suggests project files as you type (). +- Now use Positron's active runtime to choose the language for new code cells in an empty document (). ## 1.131.0 (Release on 2026-04-14) diff --git a/apps/vscode/src/@types/hooks.d.ts b/apps/vscode/src/@types/hooks.d.ts index c8b0c2b7..5b855b5a 100644 --- a/apps/vscode/src/@types/hooks.d.ts +++ b/apps/vscode/src/@types/hooks.d.ts @@ -13,6 +13,12 @@ declare module 'positron' { StatementRangeSyntaxError: typeof StatementRangeSyntaxError; } + export interface LanguageRuntimeSession { + readonly runtimeMetadata: { + readonly languageId: string; + }; + } + export interface PositronRuntime { executeCode( languageId: string, @@ -25,6 +31,8 @@ declare module 'positron' { documentUri: vscode.Uri, cellRanges: vscode.Range[] ): Thenable; + + getForegroundSession(): Thenable; } export interface PositronLanguages { diff --git a/apps/vscode/src/providers/insert.ts b/apps/vscode/src/providers/insert.ts index 564dbae3..fc9d0914 100644 --- a/apps/vscode/src/providers/insert.ts +++ b/apps/vscode/src/providers/insert.ts @@ -22,6 +22,7 @@ import { import { Command } from "../core/command"; import { isQuartoDoc } from "../core/doc"; import { MarkdownEngine } from "../markdown/engine"; +import { hooksApi } from "../host/hooks"; import { isExecutableLanguageBlock, languageBlockAtPosition, languageNameFromBlock } from "quarto-core"; @@ -98,6 +99,16 @@ class InsertCodeCellCommand implements Command { } } + // if no language found in document, fall back to Positron's active runtime + const kSupportedLanguages = ['python', 'r', 'julia', 'ojs', 'sql', 'bash', 'mermaid', 'dot']; + if (!language) { + const session = await hooksApi()?.runtime.getForegroundSession(); + const sessionLang = session?.runtimeMetadata.languageId ?? ""; + if (kSupportedLanguages.includes(sessionLang)) { + language = sessionLang; + } + } + // if we have a known language, use it and put the cursor directly in the // code cell, otherwise let the user select the language first let header; @@ -105,8 +116,7 @@ class InsertCodeCellCommand implements Command { if (language) { header = "```{" + language + "}"; } else { - const languages = ['python', 'r', 'julia', 'ojs', 'sql', 'bash', 'mermaid', 'dot']; - header = "```{${1|" + languages.join(",") + "|}}"; + header = "```{${1|" + kSupportedLanguages.join(",") + "|}}"; } // insert snippet diff --git a/apps/vscode/src/test/insert.test.ts b/apps/vscode/src/test/insert.test.ts new file mode 100644 index 00000000..c5fdf47b --- /dev/null +++ b/apps/vscode/src/test/insert.test.ts @@ -0,0 +1,96 @@ +import * as vscode from "vscode"; +import * as assert from "assert"; +import { examplesOutUri, openAndShowExamplesOutTextDocument, WORKSPACE_PATH } from "./test-utils"; + +suite("Insert Code Cell", function () { + suiteSetup(async function () { + await vscode.workspace.fs.delete(examplesOutUri(), { recursive: true }); + await vscode.workspace.fs.copy(vscode.Uri.file(WORKSPACE_PATH), examplesOutUri()); + }); + + teardown(async function () { + await vscode.commands.executeCommand("undo"); + }); + + // format/basics.qmd (0-indexed lines): + // 9: ```{python} + // 10: x = 1 + 1 <- inside python block + // 11: ``` + // 13: More markdown text. <- between blocks + // 15: ```{r} + // 16: y <- 1 + 1 <- inside r block + // 17: ``` + // 19: Final line. <- after all blocks + // 7: Some regular text here. <- before all blocks + + suite("Language from cursor context", function () { + test("Uses language of block when cursor is inside a Python block", async function () { + const { doc, editor } = await openAndShowExamplesOutTextDocument("format/basics.qmd"); + const before = countOccurrences(doc.getText(), "```{python}"); + + editor.selection = new vscode.Selection(10, 0, 10, 0); + await vscode.commands.executeCommand("quarto.insertCodeCell"); + + assert.strictEqual(countOccurrences(doc.getText(), "```{python}"), before + 1); + }); + + test("Uses language of block when cursor is inside an R block", async function () { + const { doc, editor } = await openAndShowExamplesOutTextDocument("format/basics.qmd"); + const before = countOccurrences(doc.getText(), "```{r}"); + + editor.selection = new vscode.Selection(16, 0, 16, 0); + await vscode.commands.executeCommand("quarto.insertCodeCell"); + + assert.strictEqual(countOccurrences(doc.getText(), "```{r}"), before + 1); + }); + + test("Uses language of nearest preceding block when cursor is between blocks", async function () { + const { doc, editor } = await openAndShowExamplesOutTextDocument("format/basics.qmd"); + const before = countOccurrences(doc.getText(), "```{python}"); + + editor.selection = new vscode.Selection(13, 0, 13, 0); + await vscode.commands.executeCommand("quarto.insertCodeCell"); + + assert.strictEqual(countOccurrences(doc.getText(), "```{python}"), before + 1); + }); + + test("Uses language of first following block when cursor precedes all blocks", async function () { + const { doc, editor } = await openAndShowExamplesOutTextDocument("format/basics.qmd"); + const before = countOccurrences(doc.getText(), "```{python}"); + + editor.selection = new vscode.Selection(7, 0, 7, 0); + await vscode.commands.executeCommand("quarto.insertCodeCell"); + + assert.strictEqual(countOccurrences(doc.getText(), "```{python}"), before + 1); + }); + + test("Uses language of last block when cursor follows all blocks", async function () { + const { doc, editor } = await openAndShowExamplesOutTextDocument("format/basics.qmd"); + const before = countOccurrences(doc.getText(), "```{r}"); + + editor.selection = new vscode.Selection(19, 0, 19, 0); + await vscode.commands.executeCommand("quarto.insertCodeCell"); + + assert.strictEqual(countOccurrences(doc.getText(), "```{r}"), before + 1); + }); + }); + + suite("Language picker fallback", function () { + test("Inserts a code fence when document has no executable code cells", async function () { + const content = "---\ntitle: Test\n---\n\nJust some markdown.\n"; + const uri = examplesOutUri("insert-test-empty.qmd"); + await vscode.workspace.fs.writeFile(uri, Buffer.from(content, "utf8") as Uint8Array); + + const doc = await vscode.workspace.openTextDocument(uri); + const editor = await vscode.window.showTextDocument(doc); + editor.selection = new vscode.Selection(4, 0, 4, 0); + await vscode.commands.executeCommand("quarto.insertCodeCell"); + + assert.ok(doc.getText().includes("```{")); + }); + }); +}); + +function countOccurrences(text: string, substring: string): number { + return text.split(substring).length - 1; +}