Skip to content
Open
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
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.130.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.129.0 (Release on 2026-01-29)

- Fixed Copilot completions in `.qmd` documents (<https://github.com/quarto-dev/quarto/pull/887>).
Expand Down
14 changes: 14 additions & 0 deletions apps/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,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 @@ -42,6 +42,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 @@ -168,6 +170,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