- Completion for brand values in .archify (YAML) and Archify JSON sources, from the bundled brand-marks/catalog.json (107 marks); aliases and product names match, so "kafka" offers apache-kafka - brand/unknown diagnostics carry their JSON pointer only at the start of the message; use it when subject.path is missing, so the error lands on the brand line instead of the top of the file - Unit tests for catalog loading, value detection and pointer fallback; integration checks for completion and diagnostic placement Closes #5
88 lines
3.6 KiB
TypeScript
88 lines
3.6 KiB
TypeScript
import { findNodeAtLocation, Node, parseTree } from 'jsonc-parser';
|
|
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
|
|
export interface OffsetRange {
|
|
start: number;
|
|
end: number;
|
|
}
|
|
|
|
/**
|
|
* The JSON pointer an Archify diagnostic is about. Most diagnostics carry it
|
|
* in subject.path; brand diagnostics only start their message with it.
|
|
*/
|
|
export function diagnosticPointer(diagnostic: { message: string; subject?: { path?: string } }): string | undefined {
|
|
return diagnostic.subject?.path ?? /^(\/[^\s"]+)\s/.exec(diagnostic.message)?.[1];
|
|
}
|
|
|
|
/** Splits an RFC 6901 JSON pointer into path segments. */
|
|
export function pointerSegments(pointer: string): (string | number)[] {
|
|
if (!pointer || pointer === '/') return [];
|
|
return pointer
|
|
.replace(/^\//, '')
|
|
.split('/')
|
|
.map((raw) => raw.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
.map((segment) => (/^(0|[1-9]\d*)$/.test(segment) ? Number(segment) : segment));
|
|
}
|
|
|
|
/**
|
|
* Maps a JSON pointer to a text range. Objects and arrays resolve to their
|
|
* property key (or opening bracket) so a "missing property" error does not
|
|
* underline a whole block. Unresolvable paths fall back to the deepest
|
|
* ancestor that exists.
|
|
*/
|
|
export function rangeForPointer(text: string, pointer: string | undefined): OffsetRange {
|
|
const root = parseTree(text, [], { allowTrailingComma: true });
|
|
if (!root) return { start: 0, end: 0 };
|
|
const segments = pointerSegments(pointer ?? '');
|
|
let node: Node | undefined;
|
|
for (let length = segments.length; length >= 0 && !node; length -= 1) {
|
|
node = findNodeAtLocation(root, segments.slice(0, length));
|
|
}
|
|
if (!node) return { start: 0, end: 0 };
|
|
if (node.type === 'object' || node.type === 'array') {
|
|
const key = node.parent?.type === 'property' ? node.parent.children?.[0] : undefined;
|
|
if (key) return { start: key.offset, end: key.offset + key.length };
|
|
return { start: node.offset, end: node.offset + 1 };
|
|
}
|
|
return { start: node.offset, end: node.offset + node.length };
|
|
}
|
|
|
|
/** The same mapping for YAML text (`.archify` files). */
|
|
export function rangeForPointerYaml(text: string, pointer: string | undefined): OffsetRange {
|
|
const doc = parseDocument(text, { keepSourceTokens: false, uniqueKeys: false });
|
|
let node: unknown = doc.contents;
|
|
let keyRange: [number, number] | undefined;
|
|
for (const segment of pointerSegments(pointer ?? '')) {
|
|
if (isMap(node)) {
|
|
const pair = node.items.find((item) => isScalar(item.key) && String(item.key.value) === String(segment));
|
|
if (!pair) break;
|
|
keyRange = isScalar(pair.key) && pair.key.range ? [pair.key.range[0], pair.key.range[1]] : undefined;
|
|
node = pair.value;
|
|
} else if (isSeq(node) && typeof segment === 'number' && segment < node.items.length) {
|
|
keyRange = undefined;
|
|
node = node.items[segment];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if ((isMap(node) || isSeq(node)) && keyRange) return { start: keyRange[0], end: keyRange[1] };
|
|
const range = (node as { range?: [number, number, number] } | null)?.range;
|
|
if (!range) return { start: 0, end: 0 };
|
|
if (isMap(node) || isSeq(node)) return { start: range[0], end: Math.min(range[0] + 1, range[1]) };
|
|
return { start: range[0], end: range[1] };
|
|
}
|
|
|
|
export interface SyntaxProblem extends OffsetRange {
|
|
message: string;
|
|
}
|
|
|
|
/** YAML syntax errors with their offsets (none for valid YAML). */
|
|
export function yamlSyntaxProblems(text: string): SyntaxProblem[] {
|
|
const doc = parseDocument(text, { uniqueKeys: false });
|
|
return doc.errors.map((error) => ({
|
|
start: error.pos[0],
|
|
end: Math.max(error.pos[1], error.pos[0] + 1),
|
|
message: error.message.split('\n')[0],
|
|
}));
|
|
}
|