UNPKG

@1771technologies/lytenyte-pro

Version:

Blazingly fast headless React data grid with 100s of features.

392 lines (391 loc) 14.7 kB
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 []; } }; }