Skip to content
Merged
3 changes: 3 additions & 0 deletions apps/vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 1.132.0 (Unreleased)

- Added clickable document links for file paths in `_quarto.yml` files. File paths are now clickable and navigate directly to the referenced file (<https://github.com/quarto-dev/quarto/pull/906>).
- Added filepath autocompletion in `_quarto.yml` files. When editing YAML values, the extension now suggests project files as you type (<https://github.com/quarto-dev/quarto/pull/906>).

## 1.131.0 (Release on 2026-04-14)

- Added support for Positron's statement execution feature that reports the approximate line number of the parse error (<https://github.com/quarto-dev/quarto/pull/919>).
Expand Down
14 changes: 14 additions & 0 deletions apps/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,20 @@
"default": false,
"markdownDescription": "Write markdown links as references rather than inline. Reference links are written to the location specified in `#quarto.visualEditor.markdownReferences#`."
},
"quarto.yaml.documentLinks.enabled": {
"order": 45,
"scope": "window",
"type": "boolean",
"default": true,
"markdownDescription": "Enable clickable file links in `_quarto.yml` files."
},
"quarto.yaml.filepathCompletions.enabled": {
"order": 46,
"scope": "window",
"type": "boolean",
"default": true,
"markdownDescription": "Enable file path autocomplete in `_quarto.yml` files."
},
"quarto.zotero.library": {
"order": 50,
"scope": "window",
Expand Down
8 changes: 8 additions & 0 deletions apps/vscode/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import { activateDiagram } from "./providers/diagram/diagram";
import { activateCodeFormatting } from "./providers/format";
import { activateOptionEnterProvider } from "./providers/option";
import { activateBackgroundHighlighter } from "./providers/background";
import { activateYamlLinks } from "./providers/yaml-links";
import { activateYamlFilepathCompletions } from "./providers/yaml-filepath-completions";
import { activateContextKeySetter } from "./providers/context-keys";
import { CommandManager } from "./core/command";
import { createQuartoExtensionApi, QuartoExtensionApi } from "./api";
Expand Down Expand Up @@ -205,6 +207,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<Quarto
// background highlighter
activateBackgroundHighlighter(context, engine);

// yaml document links
activateYamlLinks(context);

// yaml filepath completions
activateYamlFilepathCompletions(context);

// context setter
activateContextKeySetter(context, engine);

Expand Down
135 changes: 135 additions & 0 deletions apps/vscode/src/providers/yaml-filepath-completions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* yaml-filepath-completions.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.
*
*/

/**
* Filepath Completion Provider for Quarto YAML Configuration Files
*
* This module provides filepath autocompletion for `_quarto.yml` and `_quarto.yaml`
* configuration files. When editing YAML values (after `: ` or `- `), the extension
* suggests project files that match what you're typing.
*
* The provider:
* - Triggers completions after `: ` (key-value pairs) or `- ` (list items)
* - Scans the project directory for matching files
* - Filters suggestions based on current input
* - Ignores common non-content directories (.git, node_modules, _site, _freeze, .quarto)
*
* Can be disabled via the `quarto.yaml.filepathCompletions.enabled` setting.
*/

import * as path from "path";

import { glob } from "glob";

import {
CancellationToken,
CompletionContext,
CompletionItem,
CompletionItemKind,
CompletionItemProvider,
ExtensionContext,
languages,
Position,
Range,
TextDocument,
workspace,
} from "vscode";

const FILE_EXTENSIONS = ['qmd', 'scss', 'css', 'html', 'js', 'bib', 'tex', 'md'];
const IGNORE_PATTERNS = ['.git', 'node_modules', '_site', '_freeze', '.quarto'];

export function activateYamlFilepathCompletions(context: ExtensionContext) {
const config = workspace.getConfiguration("quarto");

if (config.get<boolean>("yaml.filepathCompletions.enabled", true)) {
context.subscriptions.push(
languages.registerCompletionItemProvider(
{ scheme: "file", pattern: "**/_quarto.{yml,yaml}" },
new QuartoYamlFilepathCompletionProvider(),
"/", "." // Trigger on path separators
)
);
}
}

class QuartoYamlFilepathCompletionProvider implements CompletionItemProvider {
async provideCompletionItems(
document: TextDocument,
position: Position,
_token: CancellationToken,
_context: CompletionContext
): Promise<CompletionItem[]> {
const line = document.lineAt(position).text;
const linePrefix = line.substring(0, position.character);

// Only provide completions after ': ' or '- '
if (!this.shouldProvideCompletions(linePrefix)) {
return [];
}

const documentDir = path.dirname(document.uri.fsPath);
const projectFiles = await getProjectFiles(documentDir);

// Get current input to filter completions
const currentInput = this.getCurrentInput(linePrefix);

return projectFiles
.filter(file => file.toLowerCase().includes(currentInput.toLowerCase()))
.map(file => {
const item = new CompletionItem(file, CompletionItemKind.File);
item.detail = "Quarto project file";
item.insertText = file;

// If there's existing input, replace it
if (currentInput) {
const startPos = position.character - currentInput.length;
item.range = new Range(
new Position(position.line, startPos),
position
);
}

return item;
});
}

private shouldProvideCompletions(linePrefix: string): boolean {
// Check if we're in a position where file path completions make sense
// After ': ' for key-value pairs or after '- ' for list items
return /(?::\s+|^\s*-\s+)\S*$/.test(linePrefix);
}

private getCurrentInput(linePrefix: string): string {
// Extract the current partial input after ': ' or '- '
const match = linePrefix.match(/(?::\s+|^\s*-\s+)(\S*)$/);
return match ? match[1] : "";
}
}

async function getProjectFiles(projectDir: string): Promise<string[]> {
const extensionPattern = `**/*.{${FILE_EXTENSIONS.join(',')}}`;

try {
const files = await glob(extensionPattern, {
cwd: projectDir,
ignore: IGNORE_PATTERNS.map(p => `**/${p}/**`),
nodir: true,
});

return files.sort();
} catch {
return [];
}
}
127 changes: 127 additions & 0 deletions apps/vscode/src/providers/yaml-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* yaml-links.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.
*
*/

/**
* Document Link Provider for Quarto YAML Configuration Files
*
* This module provides clickable document links for file paths referenced in
* `_quarto.yml` and `_quarto.yaml` configuration files. When a file path is
* detected in the YAML content (e.g., `render: hello.qmd` or `css: styles.css`),
* it becomes a clickable link that navigates directly to that file.
*
* The provider:
* - Scans YAML content for file paths after `: ` (key-value) or `- ` (list items)
* - Verifies that referenced files exist before creating links
* - Resolves relative paths from the location of the _quarto.yml file
*
* Can be disabled via the `quarto.yaml.documentLinks.enabled` setting.
*/

import path from "node:path";
import fs from "node:fs";

import {
CancellationToken,
DocumentLink,
DocumentLinkProvider,
ExtensionContext,
languages,
Position,
Range,
TextDocument,
Uri,
workspace,
} from "vscode";

const FILE_EXTENSIONS = ['qmd', 'scss', 'css', 'html', 'js', 'bib', 'tex', 'md'];
const IGNORE_PATTERNS = ['.git', 'node_modules', '_site', '_freeze', '.quarto'];

export function activateYamlLinks(context: ExtensionContext) {
const config = workspace.getConfiguration("quarto");

if (config.get<boolean>("yaml.documentLinks.enabled", true)) {
context.subscriptions.push(
languages.registerDocumentLinkProvider(
{ scheme: "file", pattern: "**/_quarto.{yml,yaml}" },
new QuartoYamlLinkProvider()
)
);
}
}

class QuartoYamlLinkProvider implements DocumentLinkProvider {
provideDocumentLinks(
document: TextDocument,
_token: CancellationToken
): DocumentLink[] {
const links: DocumentLink[] = [];
const text = document.getText();
const lines = text.split('\n');
const documentDir = path.dirname(document.uri.fsPath);

for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
const line = lines[lineIndex];
const lineLinks = this.findLinksInLine(line, lineIndex, documentDir);
links.push(...lineLinks);
}

return links;
}

private findLinksInLine(
line: string,
lineIndex: number,
documentDir: string
): DocumentLink[] {
const links: DocumentLink[] = [];

// Match file paths in YAML values
// Pattern: looks for paths after ': ' or '- ' that end with known extensions
const extensionPattern = FILE_EXTENSIONS.join('|');
const regex = new RegExp(
`(?:^\\s*-\\s*|:\\s*)([\\w./-]+\\.(?:${extensionPattern}))`,
'gi'
);

let match;
while ((match = regex.exec(line)) !== null) {
const filePath = match[1];
const startIndex = match.index + match[0].length - filePath.length;
const endIndex = startIndex + filePath.length;

// Resolve the full path
const fullPath = path.isAbsolute(filePath)
? filePath
: path.resolve(documentDir, filePath);

// Check if the file exists
if (fs.existsSync(fullPath)) {
const range = new Range(
new Position(lineIndex, startIndex),
new Position(lineIndex, endIndex)
);

const link = new DocumentLink(
range,
Uri.file(fullPath)
);
link.tooltip = `Open ${filePath}`;
links.push(link);
}
}

return links;
}
}
15 changes: 15 additions & 0 deletions apps/vscode/src/test/examples/_quarto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
project:
type: default

metadata:
title: "Test Project"

render:
- hello.qmd
- valid-basics.qmd

bibliography: references.bib

format:
html:
css: styles.css
Loading
Loading