@1771technologies/lytenyte-pro
Version:
Blazingly fast headless React data grid with 100s of features.
392 lines (391 loc) • 14.7 kB
JavaScript
const STRING_METHODS = [
{ label: "at", kind: "function" },
{ label: "charAt", kind: "function" },
{ label: "charCodeAt", kind: "function" },
{ label: "endsWith", kind: "function" },
{ label: "includes", kind: "function" },
{ label: "indexOf", kind: "function" },
{ label: "lastIndexOf", kind: "function" },
{ label: "length", kind: "number" },
{ label: "match", kind: "function" },
{ label: "padEnd", kind: "function" },
{ label: "padStart", kind: "function" },
{ label: "repeat", kind: "function" },
{ label: "replace", kind: "function" },
{ label: "replaceAll", kind: "function" },
{ label: "slice", kind: "function" },
{ label: "split", kind: "function" },
{ label: "startsWith", kind: "function" },
{ label: "substring", kind: "function" },
{ label: "toLowerCase", kind: "function" },
{ label: "toUpperCase", kind: "function" },
{ label: "trim", kind: "function" },
{ label: "trimEnd", kind: "function" },
{ label: "trimStart", kind: "function" },
];
const ARRAY_METHODS = [
{ label: "at", kind: "function" },
{ label: "concat", kind: "function" },
{ label: "entries", kind: "function" },
{ label: "every", kind: "function" },
{ label: "filter", kind: "function" },
{ label: "find", kind: "function" },
{ label: "findIndex", kind: "function" },
{ label: "flat", kind: "function" },
{ label: "flatMap", kind: "function" },
{ label: "forEach", kind: "function" },
{ label: "includes", kind: "function" },
{ label: "indexOf", kind: "function" },
{ label: "join", kind: "function" },
{ label: "keys", kind: "function" },
{ label: "length", kind: "number" },
{ label: "map", kind: "function" },
{ label: "reduce", kind: "function" },
{ label: "reduceRight", kind: "function" },
{ label: "reverse", kind: "function" },
{ label: "slice", kind: "function" },
{ label: "some", kind: "function" },
{ label: "sort", kind: "function" },
{ label: "values", kind: "function" },
];
const DATE_METHODS = [
{ label: "getTime", kind: "function" },
{ label: "toISOString", kind: "function" },
];
const NUMBER_METHODS = [
{ label: "toExponential", kind: "function" },
{ label: "toFixed", kind: "function" },
{ label: "toLocaleString", kind: "function" },
{ label: "toPrecision", kind: "function" },
{ label: "toString", kind: "function" },
{ label: "valueOf", kind: "function" },
];
function isDot(token) {
return (token.type === "Punctuation" && token.value === ".") || token.type === "OptionalChain";
}
function isClosingBracket(token) {
return token.type === "Punctuation" && token.value === "]";
}
function isCompleteValue(token) {
return (token.type === "Identifier" ||
token.type === "QuotedIdentifier" ||
token.type === "Number" ||
token.type === "String" ||
token.type === "TemplateLiteral" ||
(token.type === "Punctuation" && (token.value === ")" || token.value === "]")));
}
function isStringLiteral(token) {
return token.type === "String" || token.type === "TemplateLiteral";
}
const BINARY_OPERATORS = [
{ label: "+ Plus", kind: "operator", value: "+" },
{ label: "- Minus", kind: "operator", value: "-" },
{ label: "* Multiply", kind: "operator", value: "*" },
{ label: "/ Divide", kind: "operator", value: "/" },
{ label: "% Modulus", kind: "operator", value: "%" },
{ label: "** Exponentiation", kind: "operator", value: "**" },
{ label: "== Equal To", kind: "operator", value: "==" },
{ label: "!= Not Equal To", kind: "operator", value: "!=" },
{ label: "< Less Than", kind: "operator", value: "<" },
{ label: "<= Less Than Or Equal To", kind: "operator", value: "<=" },
{ label: "> Greater Than", kind: "operator", value: ">" },
{ label: ">= Greater Than Or Equal To", kind: "operator", value: ">=" },
{ label: "&& AND", kind: "operator", value: "&&" },
{ label: "|| OR", kind: "operator", value: "||" },
{ label: "?? OR if Null", kind: "operator", value: "??" },
{ label: "|> Pipe", kind: "operator", value: "|>" },
];
const STRING_METHOD_RETURNS = {
at: "string",
charAt: "string",
charCodeAt: "number",
endsWith: "unknown",
includes: "unknown",
indexOf: "number",
lastIndexOf: "number",
match: "array",
padEnd: "string",
padStart: "string",
repeat: "string",
replace: "string",
replaceAll: "string",
slice: "string",
split: "array",
startsWith: "unknown",
substring: "string",
toLowerCase: "string",
toUpperCase: "string",
trim: "string",
trimEnd: "string",
trimStart: "string",
};
const ARRAY_METHOD_RETURNS = {
at: "unknown",
concat: "array",
entries: "unknown",
every: "unknown",
filter: "array",
find: "unknown",
findIndex: "number",
flat: "array",
flatMap: "array",
forEach: "unknown",
includes: "unknown",
indexOf: "number",
join: "string",
keys: "unknown",
map: "array",
reduce: "unknown",
reduceRight: "unknown",
reverse: "array",
slice: "array",
some: "unknown",
sort: "array",
values: "unknown",
};
const DATE_METHOD_RETURNS = {
getTime: "number",
toISOString: "string",
};
function kindOf(value) {
if (typeof value === "function")
return "function";
if (Array.isArray(value))
return "array";
if (typeof value === "string")
return "string";
if (typeof value === "number")
return "number";
if (typeof value === "boolean")
return "boolean";
if (typeof value === "object" && value !== null)
return "object";
return "unknown";
}
function isContextEntry(v) {
return v != null && typeof v === "object" && "type" in v && "value" in v;
}
function resolveEntry(context, path) {
if (path.length === 0)
return undefined;
let entry = context[path[0]];
for (let i = 1; i < path.length; i++) {
if (!entry || entry.value == null || typeof entry.value !== "object")
return undefined;
const nested = entry.value[path[i]];
if (isContextEntry(nested)) {
entry = nested;
}
else if (nested !== undefined) {
entry = { value: nested, type: kindOf(nested) };
}
else {
return undefined;
}
}
return entry;
}
function findMatchingOpenParen(relevant, closeIndex) {
let depth = 1;
let i = closeIndex - 1;
while (i >= 0) {
if (relevant[i].type === "Punctuation") {
if (relevant[i].value === ")")
depth++;
else if (relevant[i].value === "(") {
if (--depth === 0)
return i;
}
}
i--;
}
return -1;
}
function buildPathFromTokens(relevant, i) {
const tokenName = (t) => (t.type === "QuotedIdentifier" ? t.value.slice(2, -1) : t.value);
const token = relevant[i];
if (!token || (token.type !== "Identifier" && token.type !== "QuotedIdentifier"))
return null;
const path = [tokenName(token)];
let j = i - 1;
while (j >= 1 && isDot(relevant[j])) {
j--;
if (relevant[j].type !== "Identifier" && relevant[j].type !== "QuotedIdentifier")
break;
path.unshift(tokenName(relevant[j]));
j--;
}
return path;
}
function resolveTypeOf(relevant, i, context) {
const token = relevant[i];
if (!token)
return "unknown";
if (token.type === "DateLiteral")
return "date";
if (isStringLiteral(token))
return "string";
if (isClosingBracket(token))
return "array";
if (token.type === "Punctuation" && token.value === ")") {
const openIdx = findMatchingOpenParen(relevant, i);
if (openIdx < 1)
return "unknown";
const methodToken = relevant[openIdx - 1];
if (methodToken?.type !== "Identifier")
return "unknown";
const hasDot = openIdx >= 2 && isDot(relevant[openIdx - 2]);
if (!hasDot) {
// Top-level function call: fn(...)
const funcEntry = context[methodToken.value];
if (funcEntry?.type === "function" && funcEntry.return)
return funcEntry.return;
return "unknown";
}
// Method call: receiver.method(...)
// First try resolving the receiver as a context object to find a typed method entry
const receiverPath = buildPathFromTokens(relevant, openIdx - 3);
if (receiverPath) {
const receiverEntry = resolveEntry(context, receiverPath);
if (receiverEntry?.type === "object" && receiverEntry.value != null) {
const methodVal = receiverEntry.value[methodToken.value];
const methodEntry = isContextEntry(methodVal) ? methodVal : undefined;
if (methodEntry?.type === "function" && methodEntry.return)
return methodEntry.return;
}
}
// Fall back to builtin method return-type maps (string/array prototype methods)
const receiverType = resolveTypeOf(relevant, openIdx - 3, context);
if (receiverType === "string")
return STRING_METHOD_RETURNS[methodToken.value] ?? "unknown";
if (receiverType === "array")
return ARRAY_METHOD_RETURNS[methodToken.value] ?? "unknown";
if (receiverType === "date")
return DATE_METHOD_RETURNS[methodToken.value] ?? "unknown";
return "unknown";
}
if (token.type === "Identifier" || token.type === "QuotedIdentifier") {
const path = buildPathFromTokens(relevant, i);
if (!path)
return "unknown";
const entry = resolveEntry(context, path);
const kind = entry?.type ?? "unknown";
if (kind === "string" || kind === "number" || kind === "array" || kind === "object")
return kind;
return "unknown";
}
return "unknown";
}
function analyzeTokens(tokens, cursorPosition, context) {
const relevant = tokens.filter((t) => t.end <= cursorPosition && t.type !== "EOF" && t.type !== "Whitespace");
if (relevant.length === 0)
return { kind: "top-level" };
let i = relevant.length - 1;
// Skip the partial word the user is currently typing (only if cursor is at its end)
if (relevant[i].type === "Identifier" && relevant[i].end === cursorPosition)
i--;
if (i < 0)
return { kind: "top-level" };
// If the token before the word isn't a dot, check if it's a complete value
if (!isDot(relevant[i])) {
if (isCompleteValue(relevant[i]))
return { kind: "after-value" };
return { kind: "top-level" };
}
// Step over the dot
i--;
if (i < 0)
return { kind: "none" };
const beforeDot = relevant[i];
// "hello". or `hello`. → string methods
if (isStringLiteral(beforeDot))
return { kind: "string-literal" };
// [1,2,3]. → array methods
if (isClosingBracket(beforeDot))
return { kind: "array-literal" };
// d"2023-02-11". → date methods
if (beforeDot.type === "DateLiteral")
return { kind: "date-value" };
// fn(). → resolve call return type recursively
if (beforeDot.type === "Punctuation" && beforeDot.value === ")") {
const resultType = resolveTypeOf(relevant, i, context);
if (resultType === "string")
return { kind: "string-literal" };
if (resultType === "number")
return { kind: "number-value" };
if (resultType === "array")
return { kind: "array-literal" };
if (resultType === "date")
return { kind: "date-value" };
return { kind: "none" };
}
// identifier chain: walk back through alternating Identifier / QuotedIdentifier / dot tokens
if (beforeDot.type === "Identifier" || beforeDot.type === "QuotedIdentifier") {
const tokenName = (t) => (t.type === "QuotedIdentifier" ? t.value.slice(2, -1) : t.value);
const path = [tokenName(beforeDot)];
i--;
while (i >= 1) {
if (!isDot(relevant[i]))
break;
i--;
if (relevant[i].type !== "Identifier" && relevant[i].type !== "QuotedIdentifier")
break;
path.unshift(tokenName(relevant[i]));
i--;
}
return { kind: "context-path", path };
}
return { kind: "none" };
}
const VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
function objectCompletions(obj) {
return Object.entries(obj).map(([key, val]) => ({
label: key,
kind: isContextEntry(val) ? val.type : kindOf(val),
id: key,
value: VALID_IDENTIFIER.test(key) ? undefined : `@"${key}"`,
}));
}
function builtinCompletions(methods) {
return methods.map((m) => ({ label: m.label, kind: m.kind, id: m.label, value: m.value }));
}
function binaryOperatorCompletions() {
return builtinCompletions(BINARY_OPERATORS);
}
export function createCompletionProvider(context) {
return function completionProvider(tokens, cursorPosition) {
const analysis = analyzeTokens(tokens, cursorPosition, context);
switch (analysis.kind) {
case "top-level":
return objectCompletions(context);
case "string-literal":
return builtinCompletions(STRING_METHODS);
case "number-value":
return builtinCompletions(NUMBER_METHODS);
case "date-value":
return builtinCompletions(DATE_METHODS);
case "array-literal":
return builtinCompletions(ARRAY_METHODS);
case "context-path": {
const entry = resolveEntry(context, analysis.path);
if (!entry)
return [];
if (entry.type === "string")
return builtinCompletions(STRING_METHODS);
if (entry.type === "number")
return builtinCompletions(NUMBER_METHODS);
if (entry.type === "array")
return builtinCompletions(ARRAY_METHODS);
if (entry.type === "date")
return builtinCompletions(DATE_METHODS);
if (entry.type === "object" && entry.value != null && typeof entry.value === "object")
return objectCompletions(entry.value);
return [];
}
case "after-value":
return binaryOperatorCompletions();
case "none":
return [];
}
};
}