diff --git a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx index 5f1eec3b59..98af1dc9e4 100644 --- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx @@ -20,6 +20,7 @@ import { PreviewBox } from './PreviewBox'; import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { syntaxTree } from '@codemirror/language'; import { search, searchKeymap, searchPanelOpen } from '@codemirror/search'; import { handleSearchPanel } from './SearchBox'; import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; @@ -67,7 +68,7 @@ const MultiLineCodeEditor = (props) => { const context = useContext(CodeHinterContext); - const { suggestionList } = createReferencesLookup(context, true); + const { suggestionList: paramList } = createReferencesLookup(context, true); const currentValueRef = useRef(initialValue); @@ -148,8 +149,29 @@ const MultiLineCodeEditor = (props) => { return suggestion.hint.includes(nearestSubstring); }); + const localVariables = new Set(); + + // Traverse the syntax tree to extract variable declarations + syntaxTree(context.state).iterate({ + enter: (node) => { + // JavaScript: Detect variable declarations (var, let, const) + if (node.name === 'VariableDefinition') { + const varName = context.state.sliceDoc(node.from, node.to); + if (varName && varName.startsWith(nearestSubstring)) localVariables.add(varName); + } + }, + }); + + // Convert Set to an array of completion suggestions + const localVariableSuggestions = [...localVariables].map((varName) => ({ + hint: varName, + type: 'variable', + })); + + const suggestionList = paramList.filter((paramSuggestion) => paramSuggestion.hint.includes(nearestSubstring)); + const suggestions = generateHints( - [...JSLangHints, ...autoSuggestionList, ...suggestionList], + [...localVariableSuggestions, ...JSLangHints, ...autoSuggestionList, ...suggestionList], null, nearestSubstring ).map((hint) => { @@ -206,6 +228,7 @@ const MultiLineCodeEditor = (props) => { return { from: context.pos, options: [...suggestions], + filter: false, }; } @@ -239,7 +262,7 @@ const MultiLineCodeEditor = (props) => { ]); // eslint-disable-next-line react-hooks/exhaustive-deps - const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), []); + const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [paramList]); const { handleTogglePopupExapand, isOpen, setIsOpen, forceUpdate } = portalProps; let cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : props.cyLabel; diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx index 65e6f2eadd..16514ca409 100644 --- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx @@ -1,12 +1,18 @@ /* eslint-disable import/no-unresolved */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState, useContext } from 'react'; import { PreviewBox } from './PreviewBox'; import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip'; import { useTranslation } from 'react-i18next'; import { camelCase, isEmpty, noop, get } from 'lodash'; import CodeMirror from '@uiw/react-codemirror'; import { javascript } from '@codemirror/lang-javascript'; -import { autocompletion, completionKeymap, completionStatus, acceptCompletion } from '@codemirror/autocomplete'; +import { + autocompletion, + completionKeymap, + completionStatus, + acceptCompletion, + startCompletion, +} from '@codemirror/autocomplete'; import { defaultKeymap } from '@codemirror/commands'; import { keymap } from '@codemirror/view'; import FxButton from '../CodeBuilder/Elements/FxButton'; @@ -22,6 +28,8 @@ import CodeHinter from './CodeHinter'; import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext'; +import { createReferencesLookup } from '@/_stores/utils'; import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => { @@ -73,6 +81,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r if (typeof initialValue === 'string' && (initialValue?.includes('components') || initialValue?.includes('queries'))) { newInitialValue = replaceIdsWithName(initialValue); } + //! Re render the component when the componentName changes as the initialValue is not updated // const { variablesExposedForPreview } = useContext(EditorContext) || {}; @@ -199,9 +208,14 @@ const EditorInput = ({ wrapperRef, showSuggestions, }) => { - const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow); + const codeHinterContext = useContext(CodeHinterContext); + const { suggestionList: paramHints } = createReferencesLookup(codeHinterContext, true); const getSuggestions = useStore((state) => state.getSuggestions, shallow); + const [codeMirrorView, setCodeMirrorView] = useState(undefined); + + const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow); + const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline'); const isInsideQueryManager = useMemo( @@ -209,16 +223,16 @@ const EditorInput = ({ [wrapperRef.current] ); function autoCompleteExtensionConfig(context) { - const hints = getSuggestions(); + const hintsWithoutParamHints = getSuggestions(); const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager); - const allHints = { - ...hints, - appHints: [...hints.appHints, ...serverHints], - }; - let word = context.matchBefore(/\w*/); + const hints = { + ...hintsWithoutParamHints, + appHints: [...hintsWithoutParamHints.appHints, ...serverHints, ...paramHints], + }; + const totalReferences = (context.state.doc.toString().match(/{{/g) || []).length; let queryInput = context.state.doc.toString(); @@ -247,17 +261,18 @@ const EditorInput = ({ queryInput = '{{' + currentWord + '}}'; } - let completions = getAutocompletion(queryInput, validationType, allHints, totalReferences, originalQueryInput); + let completions = getAutocompletion(queryInput, validationType, hints, totalReferences, originalQueryInput); return { from: word.from, options: completions, validFor: /^\{\{.*\}\}$/, + filter: false, }; } // eslint-disable-next-line react-hooks/exhaustive-deps - const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [isInsideQueryManager]); + const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [isInsideQueryManager, paramHints]); const autoCompleteConfig = autocompletion({ override: [overRideFunction], @@ -424,6 +439,9 @@ const EditorInput = ({ ref={previewRef} > { + setCodeMirrorView(view); + }} value={currentValue} placeholder={placeholder} height={isInsideQueryPane ? '100%' : showLineNumbers ? '400px' : '100%'} @@ -460,11 +478,16 @@ const EditorInput = ({ theme={theme} indentWithTab={false} readOnly={disabled} + onKeyDown={(event) => { + if (event.key === 'Backspace') { + startCompletion(codeMirrorView); + } + }} /> - - - + + + ); }; diff --git a/frontend/src/AppBuilder/CodeEditor/autocompleteExtensionConfig.js b/frontend/src/AppBuilder/CodeEditor/autocompleteExtensionConfig.js index e1d597c957..d845fa521e 100644 --- a/frontend/src/AppBuilder/CodeEditor/autocompleteExtensionConfig.js +++ b/frontend/src/AppBuilder/CodeEditor/autocompleteExtensionConfig.js @@ -67,7 +67,8 @@ export const getAutocompletion = (input, fieldType, hints, totalReferences = 1, originalQueryInput, searchInput ); - return orderSuggestions(suggestions, fieldType); + + return suggestions; }; function orderSuggestions(suggestions, validationType) { @@ -90,10 +91,18 @@ export const generateHints = (hints, totalReferences = 1, input, searchText) => const hasDepth = currentWord.includes('.'); const lastDepth = getLastSubstring(currentWord); - const displayLabel = getLastDepth(displayedHint); + let displayLabel = getLastDepth(displayedHint); + + if (type != 'js_method') { + const currentWordDepth = currentWord.split('.').length; + displayLabel = hint + .split('.') + .slice(currentWordDepth - 1) + .join('.'); + } return { - displayLabel: lastDepth === '' ? displayedHint : displayLabel, + displayLabel, label: displayedHint, info: displayedHint, type: type === 'js_method' ? 'js_methods' : type?.toLowerCase(), @@ -154,40 +163,24 @@ export const generateHints = (hints, totalReferences = 1, input, searchText) => }; function filterHintsByDepth(input, hints) { - if (input === '') return hints; + const inputParts = input.split('.'); + const inputDepth = inputParts.length + 1; - const inputDepth = input.includes('.') ? input.split('.').length : 0; - - const filteredHints = hints.filter((cm) => { - const hintParts = cm.hint.split('.'); - - let shouldInclude = - (cm.hint.startsWith(input) && hintParts.length === inputDepth + 1) || - (cm.hint.startsWith(input) && hintParts.length === inputDepth); - - const shouldFuzzyMatch = !shouldInclude ? hintParts.length > inputDepth : false; - - if (shouldFuzzyMatch) { - // fuzzy match - let matchedDepth = -1; - for (let i = 0; i < hintParts.length; i++) { - if (hintParts[i].includes(input)) { - matchedDepth = i; - break; - } - } - - if (matchedDepth !== -1) { - shouldInclude = hintParts.length === matchedDepth + 1; - } - } else if (input.endsWith('.')) { - shouldInclude = cm.hint.startsWith(input) && hintParts.length === inputDepth; - } - - return shouldInclude; + const hintsWithDepth = hints.map((hint) => { + const hintParts = hint.hint.split('.'); + return { + ...hint, + depth: hintParts.length, + }; }); - return filteredHints; + const filteredHints = hintsWithDepth.filter((hint) => { + return hint.depth <= inputDepth; + }); + + const sortedHints = filteredHints.sort((hint1, hint2) => hint1.depth - hint2.depth); + + return sortedHints; } export function findNearestSubstring(inputStr, currentCurosorPos) { diff --git a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js index b76c8b072d..a253346eec 100644 --- a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js @@ -122,6 +122,7 @@ export const createResolvedSlice = (set, get) => ({ 'setVariables' ); get().updateDependencyValues(`variables.${key}`); + get().checkAndSetTrueBuildSuggestionsFlag(); }, getVariable: (key, moduleId = 'canvas') => { @@ -165,6 +166,7 @@ export const createResolvedSlice = (set, get) => ({ 'setPageVariable' ); get().updateDependencyValues(`page.variables.${key}`); + get().checkAndSetTrueBuildSuggestionsFlag(); }, getPageVariable: (key, moduleId = 'canvas') => {