105 lines
4.1 KiB
TypeScript
105 lines
4.1 KiB
TypeScript
import type { Completion, CompletionSource } from "@codemirror/autocomplete";
|
|
import { hoverTooltip } from "@codemirror/view";
|
|
|
|
import {
|
|
customJsxAuthoringCatalog,
|
|
customJsxCatalogComponentByName,
|
|
getCustomJsxComponentProps,
|
|
} from "../core/component-catalog";
|
|
import { customJsxTablerIconNames } from "../core/tabler-icons";
|
|
import { ROOT_BINDINGS } from "../jsx/analyzer-language";
|
|
|
|
const enabledCatalogComponents = customJsxAuthoringCatalog.components.filter(
|
|
(component) => component.safety !== "denied",
|
|
);
|
|
|
|
const componentCompletions: Completion[] = enabledCatalogComponents.map((component) => ({
|
|
label: component.name,
|
|
type: "class",
|
|
detail: `${component.package} · ${component.category} · ${component.safety}`,
|
|
info: component.description ?? component.documentationUrl,
|
|
}));
|
|
|
|
export function getCustomJsxComponentPropCompletions(componentName: string): Completion[] {
|
|
return getCustomJsxComponentProps(componentName)
|
|
.filter((prop) => prop.name !== "key")
|
|
.map((prop) => ({
|
|
label: `${prop.name}=`,
|
|
type: "property",
|
|
detail: `${prop.type}${prop.required ? " · required" : ""}`,
|
|
info: [prop.description, prop.literalValues?.length ? `Values: ${prop.literalValues.join(", ")}` : undefined]
|
|
.filter(Boolean)
|
|
.join("\n"),
|
|
}));
|
|
}
|
|
|
|
const iconCompletions: Completion[] = customJsxTablerIconNames.map((name) => ({
|
|
label: `name="${name}"`,
|
|
type: "enum",
|
|
detail: "Safe Tabler icon",
|
|
}));
|
|
|
|
const rootBindingCompletions: Completion[] = [...ROOT_BINDINGS].toSorted().map((label) => ({
|
|
label,
|
|
type: ["data", "status", "options", "inputs"].includes(label) ? "variable" : "function",
|
|
detail: "Safe Custom JSX binding",
|
|
}));
|
|
|
|
export const createCustomJsxCompletionSource =
|
|
(additional: Completion[]): CompletionSource =>
|
|
(context) => {
|
|
const word = context.matchBefore(/[A-Za-z](?:[\w."'_-]|\[|\])*/u);
|
|
if (!word && !context.explicit) return null;
|
|
const source = context.state.doc.toString();
|
|
const componentName = activeOpeningComponent(source, context.pos);
|
|
const contextualCompletions = componentName
|
|
? [
|
|
...getCustomJsxComponentPropCompletions(componentName),
|
|
...(componentName === "TablerIcon" ? iconCompletions : []),
|
|
]
|
|
: componentCompletions;
|
|
return {
|
|
from: word?.from ?? context.pos,
|
|
options: [...rootBindingCompletions, ...contextualCompletions, ...additional],
|
|
};
|
|
};
|
|
|
|
export const customJsxComponentHover = hoverTooltip((view, position) => {
|
|
const source = view.state.doc.toString();
|
|
const token = dottedTokenAt(source, position);
|
|
if (!token) return null;
|
|
const component = customJsxCatalogComponentByName.get(token.value);
|
|
const activeComponentName = activeOpeningComponent(source, position);
|
|
const prop = activeComponentName
|
|
? getCustomJsxComponentProps(activeComponentName).find((candidate) => candidate.name === token.value)
|
|
: undefined;
|
|
if ((!component || component.safety === "denied") && !prop) return null;
|
|
return {
|
|
pos: token.from,
|
|
end: token.to,
|
|
above: true,
|
|
create: () => {
|
|
const dom = document.createElement("div");
|
|
dom.style.padding = "6px 8px";
|
|
dom.textContent = prop
|
|
? `${activeComponentName}.${prop.name}: ${prop.type}${prop.required ? " · required" : ""}${prop.description ? ` — ${prop.description}` : ""}`
|
|
: `${component?.name} · ${component?.package} · ${component?.category}${component?.description ? ` — ${component.description}` : ""}`;
|
|
return { dom };
|
|
},
|
|
};
|
|
});
|
|
|
|
function activeOpeningComponent(source: string, position: number): string | undefined {
|
|
const match = source.slice(0, position).match(/<([A-Z][A-Za-z0-9.]*)\b[^<>]*$/u);
|
|
const name = match?.[1];
|
|
return name && customJsxCatalogComponentByName.get(name)?.safety !== "denied" ? name : undefined;
|
|
}
|
|
|
|
function dottedTokenAt(source: string, position: number) {
|
|
let from = position;
|
|
let to = position;
|
|
while (from > 0 && /[A-Za-z0-9._-]/u.test(source[from - 1] ?? "")) from -= 1;
|
|
while (to < source.length && /[A-Za-z0-9._-]/u.test(source[to] ?? "")) to += 1;
|
|
return from === to ? null : { from, to, value: source.slice(from, to) };
|
|
}
|