import { FUNCTIONS, KEYWORDS, CONSTANTS, FUNCTION_META } from "./inbuilt_symbols.js"; const AUTOCOMPLETE_STYLE_ID = "mrmth-script-autocomplete-styles"; function ensureAutocompleteStyles() { if (document.getElementById(AUTOCOMPLETE_STYLE_ID)) return; const style = document.createElement("style"); style.id = AUTOCOMPLETE_STYLE_ID; style.textContent = ` .mrmth-autocomplete-tooltip { display: none; /* Řízeno přes JS */ min-width: 200px; max-width: 320px; margin-left: 8px; padding: 10px 12px; border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 8px; background: rgba(24, 28, 36, 0.98); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); font-family: monospace; font-size: 11px; color: #b3b9c1; line-height: 1.5; pointer-events: auto; } .mrmth-autocomplete-layout { display: flex !important; flex-direction: row !important; align-items: flex-start !important; position: fixed !important; /* Pozici drží POUZE tento společný obal! */ z-index: 10000 !important; pointer-events: none; } .mrmth-autocomplete { min-width: 180px; max-height: 220px; overflow-y: auto; overscroll-behavior: contain; pointer-events: auto; margin: 0; padding: 4px 0; list-style: none; border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 8px; background: rgba(18, 20, 24, 0.97); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); font-family: monospace; font-size: 12px; } .mrmth-autocomplete-item { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 5px 10px; cursor: pointer; color: #ecf0f1; } .mrmth-autocomplete-item.is-selected { background: rgba(142, 68, 173, 0.35); } .mrmth-autocomplete-item .mrmth-ac-kind { flex-shrink: 0; font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; color: #9aa0a6; } .mrmth-autocomplete-item .mrmth-ac-name { color: #c39bd3; font-weight: 600; } `; document.head.appendChild(style); } function isCursorInStringOrComment(text, cursor) { let inString = false; let stringChar = ""; let inLineComment = false; let inBlockComment = false; for (let i = 0; i < cursor; i++) { const ch = text[i]; const next = text[i + 1]; if (inLineComment) { if (ch === "\n") inLineComment = false; continue; } if (inBlockComment) { if (ch === "*" && next === "/") { inBlockComment = false; i++; } continue; } if (inString) { if (ch === "\\") { i++; continue; } if (ch === stringChar) inString = false; continue; } if (ch === "#") { inLineComment = true; continue; } if (ch === "/" && next === "*") { inBlockComment = true; i++; continue; } if (ch === '"' || ch === "'") { inString = true; stringChar = ch; } } return inString || inBlockComment; } function getWordPrefix(text, cursor) { const before = text.slice(0, cursor); const match = before.match(/[a-zA-Z_][a-zA-Z0-9_]*$/); if (!match) return null; return { word: match[0], start: cursor - match[0].length }; } function formatArgHint(meta) { if (!meta) return ""; if (meta.maxArgs === null) { if (meta.minArgs <= 1) return "(…)"; return `(${meta.minArgs}+ args)`; } if (meta.minArgs === meta.maxArgs) { if (meta.minArgs === 0) return "()"; if (meta.minArgs === 1) return "(x)"; return `(${meta.minArgs} args)`; } return `(${meta.minArgs}–${meta.maxArgs} args)`; } function buildSuggestions(prefix) { const lower = prefix.toLowerCase(); const items = []; for (const name of FUNCTIONS) { if (!name.toLowerCase().startsWith(lower)) continue; const meta = FUNCTION_META[name]; items.push({ name, kind: "function", hint: meta?.snippet || `${name}()`, detail: formatArgHint(meta), insertText: meta?.snippet || `${name}()`, }); } for (const name of KEYWORDS) { if (!name.toLowerCase().startsWith(lower)) continue; items.push({ name, kind: "keyword", hint: name, detail: "", insertText: name, }); } for (const name of CONSTANTS) { if (!name.toLowerCase().startsWith(lower)) continue; items.push({ name, kind: "constant", hint: name, detail: "", insertText: name, }); } items.sort((a, b) => { const kindOrder = { function: 0, constant: 1, keyword: 2 }; const ka = kindOrder[a.kind] ?? 9; const kb = kindOrder[b.kind] ?? 9; if (ka !== kb) return ka - kb; return a.name.localeCompare(b.name); }); return items.slice(0, 40); } function getCaretCoordinates(textarea, position) { const style = window.getComputedStyle(textarea); const div = document.createElement("div"); const props = [ "fontFamily", "fontSize", "fontWeight", "fontStyle", "lineHeight", "letterSpacing", "textTransform", "wordSpacing", "textIndent", "whiteSpace", "wordBreak", "overflowWrap", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "boxSizing", ]; div.style.position = "absolute"; div.style.visibility = "hidden"; div.style.whiteSpace = style.whiteSpace === "none" ? "pre" : "pre-wrap"; div.style.overflowWrap = style.overflowWrap; div.style.wordBreak = style.wordBreak; div.style.width = `${textarea.clientWidth}px`; for (const prop of props) { div.style[prop] = style[prop]; } const text = textarea.value.substring(0, position); const span = document.createElement("span"); span.textContent = text; div.appendChild(span); const marker = document.createElement("span"); marker.textContent = "\u200B"; div.appendChild(marker); document.body.appendChild(div); const top = marker.offsetTop; const left = marker.offsetLeft; div.remove(); return { top, left }; } function getTextareaViewportScale(textarea) { const rect = textarea.getBoundingClientRect(); const layoutHeight = textarea.offsetHeight; if (layoutHeight <= 0) return 1; return rect.height / layoutHeight; } function getViewportLineHeight(textarea, scale) { const lh = parseFloat(window.getComputedStyle(textarea).lineHeight); if (Number.isFinite(lh) && lh > 0) return lh * scale; return 18 * scale; } function getCaretScreenPosition(textarea, position) { const coords = getCaretCoordinates(textarea, position); const rect = textarea.getBoundingClientRect(); const scale = getTextareaViewportScale(textarea); const x = coords.left - textarea.scrollLeft; const y = coords.top - textarea.scrollTop-6; return { left: rect.left + x * scale, top: rect.top + y * scale, scale, }; } /** * Attach identifier autocomplete to a script textarea. * @param {HTMLTextAreaElement} textarea * @param {HTMLElement} [_containerEl] - unused; dropdown is portaled to document.body * @returns {{ destroy: () => void, close: () => void }} */ export function attachAutocomplete(textarea, _containerEl) { ensureAutocompleteStyles(); // 1. Deklarace prvků nového rozvržení let layoutEl = null; let listEl = null; // Toto plně nahradí tvůj původní samostatný "dropdown" let tooltipEl = null; // 2. Opravené vytvoření dropdownu, které se spustí ihned function createDropdown() { layoutEl = document.createElement("div"); layoutEl.className = "mrmth-autocomplete-layout"; layoutEl.style.position = "fixed"; layoutEl.style.zIndex = "10000"; layoutEl.style.display = "flex"; layoutEl.hidden = true; listEl = document.createElement("ul"); listEl.className = "mrmth-autocomplete"; listEl.addEventListener("wheel", onDropdownWheel, { passive: false }); layoutEl.appendChild(listEl); tooltipEl = document.createElement("div"); tooltipEl.className = "mrmth-autocomplete-tooltip"; tooltipEl.style.display = "none"; layoutEl.appendChild(tooltipEl); document.body.appendChild(layoutEl); } function updateTooltip(selectedItem) { if (!tooltipEl || !layoutEl) return; const meta = FUNCTION_META[selectedItem?.name] || selectedItem; if (!meta || !meta.description) { tooltipEl.style.display = "none"; return; } let argsText = `Arguments: min ${meta.minArgs ?? 0}`; if (meta.maxArgs !== undefined && meta.maxArgs !== null) { argsText += `, max ${meta.maxArgs}`; } else { argsText += "+ (variadic)"; } tooltipEl.innerHTML = `
${selectedItem.name}
${meta.description}
${argsText}
`; tooltipEl.style.display = "block"; const rect = tooltipEl.getBoundingClientRect(); if (rect.right > window.innerWidth) { layoutEl.style.flexDirection = "row-reverse"; tooltipEl.style.marginLeft = "0"; tooltipEl.style.marginRight = "8px"; } else { layoutEl.style.flexDirection = "row"; tooltipEl.style.marginLeft = "8px"; tooltipEl.style.marginRight = "0"; } } const onDropdownWheel = (e) => { e.stopPropagation(); const maxScroll = Math.max(0, listEl.scrollHeight - listEl.clientHeight); const atTop = listEl.scrollTop <= 0; const atBottom = listEl.scrollTop >= maxScroll; if ((e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) { e.preventDefault(); } }; createDropdown(); let items = []; let selectedIndex = 0; let prefixInfo = null; let open = false; const close = () => { open = false; if (layoutEl) { layoutEl.hidden = true; layoutEl.style.visibility = "hidden"; } if (listEl) listEl.innerHTML = ""; if (tooltipEl) tooltipEl.style.display = "none"; items = []; selectedIndex = 0; prefixInfo = null; }; const openWith = (nextItems, nextPrefix) => { if (!nextItems.length) { close(); return; } items = nextItems; selectedIndex = 0; prefixInfo = nextPrefix; open = true; if (layoutEl) { layoutEl.hidden = false; layoutEl.style.display = "flex"; // ZNOVU ROZSVÍTÍ KRESLENÍ PŘI PSANÍ } render(); positionDropdown(); }; const scrollSelectedIntoView = () => { const selected = layoutEl.querySelector(".mrmth-autocomplete-item.is-selected"); if (!selected) return; const itemTop = selected.offsetTop; const itemBottom = itemTop + selected.offsetHeight; const viewTop = listEl.scrollTop; const viewBottom = viewTop + listEl.clientHeight; const pad = 2; if (itemTop < viewTop + pad) { listEl.scrollTop = Math.max(0, itemTop - pad); } else if (itemBottom > viewBottom - pad) { listEl.scrollTop = itemBottom - listEl.clientHeight + pad; } }; const render = () => { listEl.innerHTML = ""; if (!items || items.length === 0) { close(); return; } listEl.innerHTML = ""; items.forEach((item, index) => { const li = document.createElement("li"); li.className = `mrmth-autocomplete-item is-${item.kind}${index === selectedIndex ? " is-selected" : ""}`; li.dataset.index = String(index); const left = document.createElement("span"); left.className = "mrmth-ac-name"; left.textContent = item.name; const right = document.createElement("span"); right.className = "mrmth-ac-hint"; right.textContent = item.detail || item.hint; const kind = document.createElement("span"); kind.className = "mrmth-ac-kind"; kind.textContent = item.kind; li.appendChild(left); li.appendChild(right); li.appendChild(kind); li.addEventListener("mousedown", (e) => { e.preventDefault(); selectedIndex = index; applySelected(); }); li.addEventListener("mouseenter", () => { selectedIndex = index; listEl.querySelectorAll(".mrmth-autocomplete-item").forEach((el, i) => { el.classList.toggle("is-selected", i === index); }); updateTooltip(item); }); listEl.appendChild(li); }); scrollSelectedIntoView(); if (items[selectedIndex]) { updateTooltip(items[selectedIndex]); } else { tooltipEl.style.display = "none"; } if (open) positionDropdown(); }; const positionDropdown = () => { if (!prefixInfo || !layoutEl || !listEl) return; // Změna kontroly prvků const cursorPos = textarea.selectionStart; const caret = getCaretScreenPosition(textarea, cursorPos); const textareaRect = textarea.getBoundingClientRect(); const scale = caret.scale ?? getTextareaViewportScale(textarea); const lineHeight = getViewportLineHeight(textarea, scale); const gap = 6; const viewportPad = 8; const preferredMax = 220; const minListHeight = 48; const lineTop = caret.top; const lineBottom = lineTop + lineHeight; const spaceBelow = window.innerHeight - lineBottom - gap - viewportPad; const spaceAbove = lineTop - gap - viewportPad; const spaceRight = window.innerWidth - textareaRect.right - gap - viewportPad; let placement = "below"; if (spaceBelow < minListHeight) { if (spaceRight >= 160) { placement = "right"; } else if (spaceAbove > spaceBelow + 40) { placement = "above"; } } let maxHeight; let top; let left; const dropdownWidthTarget = Math.max(180, textareaRect.width); if (placement === "below") { maxHeight = Math.min(preferredMax, Math.max(minListHeight, spaceBelow)); top = lineBottom + gap; left = caret.left; } else if (placement === "above") { maxHeight = Math.min(preferredMax, Math.max(minListHeight, spaceAbove)); top = lineTop - gap - maxHeight; if (top < viewportPad) { maxHeight = Math.max(minListHeight, lineTop - gap - viewportPad); top = lineTop - gap - maxHeight; } left = caret.left; } else { maxHeight = Math.min(preferredMax, window.innerHeight - viewportPad * 2); top = Math.max(viewportPad, Math.min(lineTop, window.innerHeight - maxHeight - viewportPad)); left = textareaRect.right + gap; } listEl.style.width = `260px`; listEl.style.maxHeight = `${maxHeight}px`; layoutEl.style.top = `${top}px`; layoutEl.style.visibility = "hidden"; layoutEl.style.left = "0px"; const dropdownWidth = 260; left = Math.max(viewportPad, Math.min(left, window.innerWidth - dropdownWidth - viewportPad)); layoutEl.style.left = `${left}px`; layoutEl.style.visibility = ""; }; const applySelected = () => { if (selectedIndex < 0 || selectedIndex >= items.length) return; const item = items[selectedIndex]; const text = textarea.value; const before = text.slice(0, prefixInfo.start); const after = text.slice(textarea.selectionStart); textarea.value = before + item.insertText + after; textarea.selectionStart = textarea.selectionEnd = before.length + item.insertText.length; textarea.dispatchEvent(new Event("input", { bubbles: true })); close(); }; const maybeOpen = (force = false) => { const cursor = textarea.selectionStart; if (cursor !== textarea.selectionEnd) { close(); return; } if (isCursorInStringOrComment(textarea.value, cursor)) { close(); return; } const info = getWordPrefix(textarea.value, cursor); if (!info || (!force && info.word.length < 1)) { close(); return; } const suggestions = buildSuggestions(info.word); if (!suggestions.length) { close(); return; } openWith(suggestions, info); }; const onInput = () => { if (!open) { const info = getWordPrefix(textarea.value, textarea.selectionStart); if (info && info.word.length >= 2) { maybeOpen(false); } return; } maybeOpen(false); }; const onKeyDown = (e) => { if (e.ctrlKey && e.key === " ") { e.preventDefault(); if (typeof maybeOpen === "function") { maybeOpen(true); } else { const text = textarea.value; const cursor = textarea.selectionStart; const prefix = getWordPrefix(text, cursor); if (prefix) { const suggestions = buildSuggestions(prefix.word); if (suggestions.length > 0) openWith(suggestions, prefix); } } return; } if (!open) return; if (e.key === "ArrowDown") { e.preventDefault(); selectedIndex = (selectedIndex + 1) % items.length; render(); return; } if (e.key === "ArrowUp") { e.preventDefault(); selectedIndex = (selectedIndex - 1 + items.length) % items.length; render(); return; } if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); applySelected(); return; } if (e.key === "Escape") { e.preventDefault(); close(); } }; const onBlur = () => { setTimeout(close, 120); }; const onReposition = () => { if (open) positionDropdown(); }; textarea.addEventListener("input", typeof onInput === "function" ? onInput : () => { const text = textarea.value; const cursor = textarea.selectionStart; const prefix = getWordPrefix(text, cursor); if (!prefix) { close(); return; } const suggestions = buildSuggestions(prefix.word); if (suggestions.length > 0) openWith(suggestions, prefix); else close(); }); textarea.addEventListener("keydown", onKeyDown); textarea.addEventListener("blur", onBlur); textarea.addEventListener("scroll", onReposition); window.addEventListener("scroll", onReposition, true); window.addEventListener("resize", onReposition); return { close, destroy() { close(); window.removeEventListener("scroll", onReposition, true); window.removeEventListener("resize", onReposition); if (layoutEl) { layoutEl.remove(); } textarea.removeEventListener("keydown", onKeyDown); textarea.removeEventListener("blur", onBlur); textarea.removeEventListener("scroll", onReposition); if (typeof onInput === "function") { textarea.removeEventListener("input", onInput); } }, }; } export { buildSuggestions, getWordPrefix, isCursorInStringOrComment, formatArgHint };