import * as vscode from 'vscode'; import { RegistryElement } from '../model/format'; import { ModelService } from '../model/service'; export const REF_SELECTOR: vscode.DocumentSelector = [{ pattern: '**/*.archify' }]; /** `ref: api` or `{ ref: ap` → what has been typed of the value. */ export function refValueAt(linePrefix: string): { typed: string } | undefined { const match = /(?:^|[\s{,])ref:\s*["']?([\w.-]*)$/.exec(linePrefix); return match ? { typed: match[1] } : undefined; } /** The `ref` value under a position, with its range, or undefined. */ export function refAt(line: string, character: number): { id: string; start: number; end: number } | undefined { const match = /(?:^|[\s{,])ref:\s*["']?([\w.-]+)/.exec(line); if (!match) return undefined; const start = match.index + match[0].length - match[1].length; const end = start + match[1].length; return character >= start && character <= end ? { id: match[1], start, end } : undefined; } function describe(element: RegistryElement): vscode.MarkdownString { const md = new vscode.MarkdownString(); md.appendMarkdown(`**${element.label ?? element.id}**${element.sublabel ? ` · ${element.sublabel}` : ''}${element.type ? ` \`${element.type}\`` : ''}\n\n`); if (element.description) md.appendMarkdown(`${element.description.trim()}\n\n`); if (element.diagram) md.appendMarkdown(`Opens: \`${element.diagram}\`\n\n`); md.appendMarkdown(`*Defined in ${element.file}, line ${element.line + 1}*`); return md; } /** Completion, hover and go-to-definition for `ref:` values in diagrams of a model. */ export class RefLanguage implements vscode.CompletionItemProvider, vscode.HoverProvider, vscode.DefinitionProvider { constructor(private readonly models: ModelService) {} async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): Promise { const value = refValueAt(document.lineAt(position.line).text.slice(0, position.character)); if (!value) return undefined; const found = await this.models.registryFor(document.uri); if (!found) return undefined; const range = new vscode.Range(position.translate(0, -value.typed.length), position); return [...found.registry.elements.values()].map((element) => { const item = new vscode.CompletionItem({ label: element.id, description: element.label }, vscode.CompletionItemKind.Reference); item.detail = [element.label, element.sublabel, element.type].filter(Boolean).join(' · '); item.documentation = describe(element); item.filterText = [element.id, element.label ?? '', element.sublabel ?? ''].join(' '); item.range = range; return item; }); } async provideHover(document: vscode.TextDocument, position: vscode.Position): Promise { const ref = refAt(document.lineAt(position.line).text, position.character); if (!ref) return undefined; const found = await this.models.registryFor(document.uri); const element = found?.registry.elements.get(ref.id); const range = new vscode.Range(position.line, ref.start, position.line, ref.end); if (!element) { return new vscode.Hover(found ? `Unknown element \`${ref.id}\`: no registry of this model defines it.` : `\`${ref.id}\` cannot be resolved: this file is not inside a model.`, range); } return new vscode.Hover(describe(element), range); } async provideDefinition(document: vscode.TextDocument, position: vscode.Position): Promise { const ref = refAt(document.lineAt(position.line).text, position.character); if (!ref) return undefined; const found = await this.models.registryFor(document.uri); const element = found?.registry.elements.get(ref.id); if (!found || !element) return undefined; const file = vscode.Uri.joinPath(found.root, ...element.file.split(/[\\/]/)); return new vscode.Location(file, new vscode.Position(element.line, 2)); } }