Skip to content

Commit 1fa78d6

Browse files
authored
feat: extend schema providers to metadata-files included YAML (#348)
1 parent a887413 commit 1fa78d6

7 files changed

Lines changed: 686 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- feat: support a v2 extension schema vocabulary at `https://m.canouil.dev/quarto-wizard/assets/schema/v2/extension-schema.json`. v2 uses JSON Schema canonical names exclusively (`minimum`, `maximum`, `multipleOf`, `additionalProperties`, `propertyNames`, `dependentRequired`, `contentEncoding`, `contentMediaType`, `replaceWith`, ...), drops `pattern-exact` in favour of `^...$` anchors in the pattern itself, and lets shortcode entries declare a parent-level `required: [name1, ...]` array. v1 schemas keep working under the v1 URI; the runtime dispatches on the instance `$schema` URI.
88
- feat: extend the v1 schema vocabulary with new optional field properties: `title`, `examples`, `format`, `null` type, `multiple-of`, `additional-properties`, `property-names`, `dependent-required`, `content-encoding`, `content-media-type`. `completion.type: directory` is now recognised alongside `file` / `color` / ... .
99
- feat: `$schema:` completion in `_schema.yml` offers both the v1 and v2 URIs (v2 preferred).
10+
- feat: extend schema completion, diagnostics, and hover to YAML files included via `metadata-files:`. Secondary configuration files declared in `_quarto.yml`, `_metadata.yml`, or `.qmd` front-matter now receive the same IntelliSense as canonical Quarto config files.
1011

1112
### Bug Fixes
1213

src/providers/registerYamlProviders.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { SchemaDiagnosticsProvider } from "./schemaDiagnosticsProvider";
88
import { SchemaDefinitionCompletionProvider, SCHEMA_DEFINITION_SELECTOR } from "./schemaDefinitionCompletionProvider";
99
import { logMessage } from "../utils/log";
1010
import { invalidateWorkspaceSchemaIndex } from "../utils/workspaceSchemaIndex";
11-
import { findOwningProjectRootSync } from "../utils/projectRootsRegistry";
11+
import { findOwningProjectRoot, findOwningProjectRootSync } from "../utils/projectRootsRegistry";
12+
import { invalidateMetadataFiles, isQmdFile, refreshSource } from "../utils/metadataFilesRegistry";
1213

1314
/**
1415
* Register YAML completion and diagnostics providers for Quarto
@@ -70,5 +71,86 @@ export function registerYamlProviders(context: vscode.ExtensionContext, schemaCa
7071
context.subscriptions.push(schemaWatcher.onDidDelete(invalidateAndRevalidate));
7172
context.subscriptions.push(schemaWatcher);
7273

74+
const applyMetadataChange = (owningRoot: string) => {
75+
invalidateWorkspaceSchemaIndex(owningRoot);
76+
diagnosticsProvider.revalidateAll();
77+
};
78+
79+
// Watch Quarto config sources for changes in `metadata-files:` entries.
80+
const metadataSourceWatcher = vscode.workspace.createFileSystemWatcher("**/_{quarto,metadata}.{yml,yaml}");
81+
const refreshMetadataSource = async (uri: vscode.Uri) => {
82+
if (uri.scheme !== "file") {
83+
return;
84+
}
85+
const owningRoot = findOwningProjectRootSync(uri.fsPath);
86+
if (!owningRoot) {
87+
return;
88+
}
89+
const changed = await refreshSource(owningRoot, uri.fsPath);
90+
if (!changed) {
91+
return;
92+
}
93+
applyMetadataChange(owningRoot);
94+
logMessage(`Metadata-files registry refreshed for ${uri.fsPath}.`, "debug");
95+
};
96+
context.subscriptions.push(metadataSourceWatcher.onDidChange(refreshMetadataSource));
97+
context.subscriptions.push(metadataSourceWatcher.onDidCreate(refreshMetadataSource));
98+
context.subscriptions.push(metadataSourceWatcher.onDidDelete(refreshMetadataSource));
99+
context.subscriptions.push(metadataSourceWatcher);
100+
101+
// `.qmd` front-matter can also list metadata-files; refresh on save/open.
102+
// Uses the async project-root lookup so first-open during activation succeeds
103+
// before tree-view discovery has populated the sync snapshot.
104+
const refreshQmdSource = async (document: vscode.TextDocument): Promise<string | undefined> => {
105+
if (document.uri.scheme !== "file") {
106+
return undefined;
107+
}
108+
if (document.languageId !== "quarto" && !isQmdFile(document.fileName)) {
109+
return undefined;
110+
}
111+
const owningRoot = await findOwningProjectRoot(document.uri);
112+
if (!owningRoot) {
113+
return undefined;
114+
}
115+
const changed = await refreshSource(owningRoot, document.uri.fsPath);
116+
return changed ? owningRoot : undefined;
117+
};
118+
const refreshQmdAndInvalidate = async (document: vscode.TextDocument) => {
119+
const changedRoot = await refreshQmdSource(document);
120+
if (changedRoot) {
121+
applyMetadataChange(changedRoot);
122+
}
123+
};
124+
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(refreshQmdAndInvalidate));
125+
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(refreshQmdAndInvalidate));
126+
127+
// Prime the registry from already-open .qmd documents in one batched pass:
128+
// invalidate each affected root once instead of N times.
129+
const primeOpenQmdDocuments = async () => {
130+
const results = await Promise.all(vscode.workspace.textDocuments.map(refreshQmdSource));
131+
const changedRoots = new Set<string>();
132+
for (const root of results) {
133+
if (root) {
134+
changedRoots.add(root);
135+
}
136+
}
137+
for (const root of changedRoots) {
138+
invalidateWorkspaceSchemaIndex(root);
139+
}
140+
if (changedRoots.size > 0) {
141+
diagnosticsProvider.revalidateAll();
142+
}
143+
};
144+
primeOpenQmdDocuments().catch((error) =>
145+
logMessage(`Failed to prime metadata-files registry: ${String(error)}.`, "warn"),
146+
);
147+
148+
// Workspace folder changes can invalidate project-root identity; rebuild lazily.
149+
context.subscriptions.push(
150+
vscode.workspace.onDidChangeWorkspaceFolders(() => {
151+
invalidateMetadataFiles();
152+
}),
153+
);
154+
73155
logMessage("YAML completion, hover, and diagnostics providers registered.", "debug");
74156
}

src/providers/yamlCompletionProvider.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { isFilePathDescriptor, buildFilePathCompletions } from "../utils/filePat
77
import { hasCompletableValues } from "../utils/schemaDocumentation";
88
import { getWorkspaceSchemaIndex } from "../utils/workspaceSchemaIndex";
99
import { findOwningProjectRoot } from "../utils/projectRootsRegistry";
10+
import { isRelevantYaml } from "../utils/metadataFilesRegistry";
1011
import { logMessage } from "../utils/log";
1112

1213
/**
@@ -21,6 +22,10 @@ export class YamlCompletionProvider implements vscode.CompletionItemProvider {
2122
position: vscode.Position,
2223
): Promise<vscode.CompletionItem[] | undefined> {
2324
try {
25+
if (!isRelevantYaml(document)) {
26+
return undefined;
27+
}
28+
2429
const lines = document.getText().split("\n");
2530
const languageId = document.languageId;
2631

@@ -409,10 +414,12 @@ export class YamlCompletionProvider implements vscode.CompletionItemProvider {
409414
}
410415

411416
/**
412-
* File patterns for YAML documents that may contain Quarto configuration.
417+
* Document selector for schema-driven YAML providers. Targets `.qmd` files
418+
* and any YAML file; per-document relevance (canonical `_quarto.*`/
419+
* `_metadata.*` filenames or `metadata-files:` registered paths) is enforced
420+
* inside each provider via {@link isRelevantYaml}.
413421
*/
414422
export const YAML_DOCUMENT_SELECTOR: vscode.DocumentSelector = [
415-
{ language: "yaml", pattern: "**/_quarto.{yml,yaml}" },
416-
{ language: "yaml", pattern: "**/_metadata.{yml,yaml}" },
423+
{ language: "yaml", scheme: "file" },
417424
{ language: "quarto", pattern: "**/*.qmd" },
418425
];

src/providers/yamlDiagnosticsProvider.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { logMessage } from "../utils/log";
88
import { debounce } from "../utils/debounce";
99
import { getWorkspaceSchemaIndex } from "../utils/workspaceSchemaIndex";
1010
import { findOwningProjectRoot } from "../utils/projectRootsRegistry";
11+
import { isRelevantYaml } from "../utils/metadataFilesRegistry";
1112

1213
/**
1314
* Validate a single value against a field descriptor, returning error messages.
@@ -180,19 +181,7 @@ export class YamlDiagnosticsProvider implements vscode.Disposable {
180181
}
181182

182183
private isRelevantDocument(document: vscode.TextDocument): boolean {
183-
const fileName = document.fileName;
184-
if (document.languageId === "quarto" || fileName.endsWith(".qmd")) {
185-
return true;
186-
}
187-
if (document.languageId === "yaml") {
188-
return (
189-
fileName.endsWith("_quarto.yml") ||
190-
fileName.endsWith("_quarto.yaml") ||
191-
fileName.endsWith("_metadata.yml") ||
192-
fileName.endsWith("_metadata.yaml")
193-
);
194-
}
195-
return false;
184+
return isRelevantYaml(document);
196185
}
197186

198187
private async validateDocument(document: vscode.TextDocument): Promise<void> {

src/providers/yamlHoverProvider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getYamlKeyPath, isInYamlRegion } from "../utils/yamlPosition";
66
import { logMessage } from "../utils/log";
77
import { getWorkspaceSchemaIndex } from "../utils/workspaceSchemaIndex";
88
import { findOwningProjectRoot } from "../utils/projectRootsRegistry";
9+
import { isRelevantYaml } from "../utils/metadataFilesRegistry";
910

1011
/**
1112
* Provides hover information for Quarto extension options
@@ -16,6 +17,10 @@ export class YamlHoverProvider implements vscode.HoverProvider {
1617

1718
async provideHover(document: vscode.TextDocument, position: vscode.Position): Promise<vscode.Hover | null> {
1819
try {
20+
if (!isRelevantYaml(document)) {
21+
return null;
22+
}
23+
1924
const lines = document.getText().split("\n");
2025
const languageId = document.languageId;
2126

0 commit comments

Comments
 (0)