/* eslint-disable import/no-unresolved */ import React, { useContext, useEffect, useMemo, useRef } from 'react'; import CodeMirror from '@uiw/react-codemirror'; import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import { defaultKeymap, indentWithTab } from '@codemirror/commands'; import { keymap } from '@codemirror/view'; import { completionKeymap, acceptCompletion, autocompletion, completionStatus } from '@codemirror/autocomplete'; import { python } from '@codemirror/lang-python'; import { sql } from '@codemirror/lang-sql'; import _ from 'lodash'; import { sass, sassCompletionSource } from '@codemirror/lang-sass'; import { okaidia } from '@uiw/codemirror-theme-okaidia'; import { githubLight } from '@uiw/codemirror-theme-github'; import { findNearestSubstring, generateHints } from './autocompleteExtensionConfig'; import ErrorBoundary from '@/_ui/ErrorBoundary'; import CodeHinter from './CodeHinter'; import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext'; import { createReferencesLookup } from '@/_stores/utils'; import { PreviewBox } from './PreviewBox'; import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { search, searchKeymap, searchPanelOpen } from '@codemirror/search'; import { handleSearchPanel } from './SearchBox'; import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; import { isInsideParent } from './utils'; import { CodeHinterBtns } from './CodehinterOverlayTriggers'; const langSupport = Object.freeze({ javascript: javascript(), python: python(), sql: sql(), jsx: javascript({ jsx: true }), css: sass(), }); const MultiLineCodeEditor = (props) => { const { darkMode, height, initialValue, lang, className, onChange, componentName, lineNumbers, placeholder, hideSuggestion, portalProps, showPreview, paramLabel = '', delayOnChange = true, // Added this prop to immediately update the onBlurUpdate callback readOnly = false, editable = true, renderCopilot, } = props; const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow); const wrapperRef = useRef(null); const getSuggestions = useStore((state) => state.getSuggestions, shallow); const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow); const isInsideQueryPane = !!document.querySelector('.code-hinter-wrapper')?.closest('.query-details'); const isInsideQueryManager = useMemo( () => isInsideParent(wrapperRef?.current, 'query-manager'), [wrapperRef.current] ); const context = useContext(CodeHinterContext); const { suggestionList } = createReferencesLookup(context, true); const currentValueRef = useRef(initialValue); const handleChange = (val) => (currentValueRef.current = val); const [editorView, setEditorView] = React.useState(null); const [isSearchPanelOpen, setIsSearchPanelOpen] = React.useState(false); const { queryPanelKeybindings } = useQueryPanelKeyHooks(onChange, currentValueRef, 'multiline'); const handleOnBlur = () => { if (!delayOnChange) return onChange(currentValueRef.current); setTimeout(() => { onChange(currentValueRef.current); }, 100); // eslint-disable-next-line react-hooks/exhaustive-deps }; const heightInPx = typeof height === 'string' && height?.includes('px') ? height : `${height}px`; const theme = darkMode ? okaidia : githubLight; const langExtention = langSupport[lang] ?? null; const setupConfig = { lineNumbers: lineNumbers ?? true, syntaxHighlighting: true, bracketMatching: true, foldGutter: true, highlightActiveLine: false, autocompletion: hideSuggestion ?? true, highlightActiveLineGutter: false, defaultKeymap: false, completionKeymap: true, searchKeymap: false, }; function autoCompleteExtensionConfig(context) { const currentCursor = context.pos; const currentString = context.state.doc.text; const inputStr = currentString.join(' '); const currentCurosorPos = currentCursor; const nearestSubstring = removeNestedDoubleCurlyBraces(findNearestSubstring(inputStr, currentCurosorPos)); const hints = getSuggestions(); const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager); const allHints = { ...hints, appHints: [...hints.appHints, ...serverHints], }; let JSLangHints = []; if (lang === 'javascript') { JSLangHints = Object.keys(allHints['jsHints']) .map((key) => { return hints['jsHints'][key]['methods'].map((hint) => ({ hint: hint, type: 'js_method', })); }) .flat(); JSLangHints = JSLangHints.filter((cm) => { let lastWordAfterDot = nearestSubstring.split('.'); lastWordAfterDot = lastWordAfterDot[lastWordAfterDot.length - 1]; if (cm.hint.includes(lastWordAfterDot)) return true; }); } const appHints = allHints['appHints']; let autoSuggestionList = appHints.filter((suggestion) => { return suggestion.hint.includes(nearestSubstring); }); const suggestions = generateHints( [...JSLangHints, ...autoSuggestionList, ...suggestionList], null, nearestSubstring ).map((hint) => { if (hint.label.startsWith('client') || hint.label.startsWith('server')) return; delete hint['apply']; hint.apply = (view, completion, from, to) => { /** * This function applies an auto-completion logic to a text editing view based on user interaction. * It uses a pre-defined completion object and modifies the document's content accordingly. * * Parameters: * - view: The editor view where the changes will be applied. * - completion: An object containing details about the completion to be applied. Includes properties like 'label' (the text to insert) and 'type' (e.g., 'js_methods'). * - from: The initial position (index) in the document where the completion starts. * - to: The position (index) in the document where the completion ends. * * Logic: * - The function calculates the start index for the change by subtracting the length of the word to be replaced (finalQuery) from the 'from' index. * - It configures the completion details such as where to insert the text and the exact text to insert. * - If the completion type is 'js_methods', it adjusts the insertion point to the 'to' index and sets the cursor position after the inserted text. * - Finally, it dispatches these configurations to the editor view to apply the changes. * * The dispatch configuration (dispacthConfig) includes changes and, optionally, the cursor selection position if the type is 'js_methods'. */ const wordToReplace = nearestSubstring; const fromIndex = from - wordToReplace.length; const pickedCompletionConfig = { from: fromIndex === 1 ? 0 : fromIndex, to: to, insert: completion.label, }; const dispacthConfig = { changes: pickedCompletionConfig, }; if (completion.type === 'js_methods') { pickedCompletionConfig.from = to; dispacthConfig.selection = { anchor: pickedCompletionConfig.to + completion.label.length - 1, }; } view.dispatch(dispacthConfig); }; return hint; }); return { from: context.pos, options: [...suggestions], }; } const customKeyMaps = [ ...defaultKeymap.filter((keyBinding) => keyBinding.key !== 'Mod-Enter'), // Remove default keybinding for Mod-Enter ...completionKeymap, ...searchKeymap, ]; const customTabKeymap = keymap.of([ { key: 'Tab', run: (view) => { if (completionStatus(view.state)) { return acceptCompletion(view); } const { state } = view; const { selection } = state; const { anchor } = selection.main; const tabSize = 2; view.dispatch({ changes: { from: anchor, insert: ' '.repeat(tabSize) }, selection: { anchor: anchor + tabSize }, }); return true; }, }, ...queryPanelKeybindings, ]); // eslint-disable-next-line react-hooks/exhaustive-deps const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), []); const { handleTogglePopupExapand, isOpen, setIsOpen, forceUpdate } = portalProps; let cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : props.cyLabel; const initialValueWithReplacedIds = useMemo(() => { if ( typeof initialValue === 'string' && (initialValue?.includes('components') || initialValue?.includes('queries')) ) { return replaceIdsWithName(initialValue); } return initialValue; }, [initialValue, replaceIdsWithName]); return (
setEditorView(view)} onUpdate={(view) => setIsSearchPanelOpen(searchPanelOpen(view.state))} />
{showPreview && (
null} componentId={null} setErrorMessage={() => null} />
)}
); }; export default MultiLineCodeEditor;