Merge pull request #12889 from ToolJet/feat/override-codemirror-autocomplete-default-filter

Feat/override codemirror autocomplete default filter
This commit is contained in:
Johnson Cherian 2025-05-16 17:22:08 +05:30 committed by GitHub
commit 77bf79f1c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 92 additions and 51 deletions

View file

@ -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;

View file

@ -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}
>
<CodeMirror
onCreateEditor={(view) => {
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);
}
}}
/>
</div>
</ErrorBoundary>
</CodeHinter.Portal>
</div>
</ErrorBoundary >
</CodeHinter.Portal >
</div >
);
};

View file

@ -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) {

View file

@ -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') => {