feat: Trigger autocomplete even outside bindings (#39446)
## Description
Shows autocomplete with results in binding brackets `{{ <result> }}`
when the user is typing something outside the binding
Fixes #39112
## Automation
/ok-to-test tags="@tag.All"
### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/13699728311>
> Commit: cd3816b05a8e36e7218af2ab6fb4c23046ab99ba
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13699728311&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Thu, 06 Mar 2025 14:46:52 UTC
<!-- end of auto-generated comment: Cypress test results -->
## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Introduced a new utility function for determining when to show
autocomplete suggestions in the code editor.
- Enhanced the code editor autocomplete for more context-aware
suggestions and improved cursor handling, resulting in a smoother
editing experience.
- **Refactor**
- Standardized input formatting across components to ensure consistent
handling of data in widget interactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
7927d762c8
commit
a3794dc0ea
|
|
@ -83,7 +83,7 @@ describe(
|
|||
_.entityExplorer.DragDropWidgetNVerify(_.draggableWidgets.JSONFORM);
|
||||
_.propPane.EnterJSContext(
|
||||
"sourcedata",
|
||||
JSON.stringify(schema),
|
||||
JSON.stringify(schema) + " ",
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ describe(
|
|||
entityExplorer.DragDropWidgetNVerify(draggableWidgets.JSONFORM, 300, 100);
|
||||
propPane.EnterJSContext(
|
||||
"sourcedata",
|
||||
JSON.stringify(schema),
|
||||
JSON.stringify(schema) + " ",
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ describe(
|
|||
);
|
||||
|
||||
// remove all table data
|
||||
propPane.UpdatePropertyFieldValue("Table data", `[]`);
|
||||
propPane.UpdatePropertyFieldValue("Table data", `[] `);
|
||||
|
||||
// allow adding a row
|
||||
propPane.TogglePropertyState("Allow adding a row", "On");
|
||||
|
|
|
|||
|
|
@ -3,25 +3,21 @@ import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
|
|||
import type { WidgetEntity, ActionEntity } from "ee/entities/DataTree/types";
|
||||
import { trim } from "lodash";
|
||||
import { getDynamicStringSegments } from "utils/DynamicBindingUtils";
|
||||
import { EditorSize } from "./EditorConfig";
|
||||
import { EditorModes, EditorSize } from "./EditorConfig";
|
||||
import { SlashCommandMenuOnFocusWidgetProps } from "./constants";
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const removeNewLineChars = (inputValue: any) => {
|
||||
export const removeNewLineChars = (inputValue: string) => {
|
||||
return inputValue && inputValue.replace(/(\r\n|\n|\r)/gm, "");
|
||||
};
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const getInputValue = (inputValue: any) => {
|
||||
export const getInputValue = (inputValue: unknown): string => {
|
||||
if (typeof inputValue === "object" || typeof inputValue === "boolean") {
|
||||
inputValue = JSON.stringify(inputValue, null, 2);
|
||||
} else if (typeof inputValue === "number" || typeof inputValue === "string") {
|
||||
inputValue += "";
|
||||
}
|
||||
|
||||
return inputValue;
|
||||
return String(inputValue || "");
|
||||
};
|
||||
const computeCursorIndex = (editor: CodeMirror.Editor) => {
|
||||
const cursor = editor.getCursor();
|
||||
|
|
@ -170,3 +166,26 @@ export function shouldShowSlashCommandMenu(
|
|||
SlashCommandMenuOnFocusWidgetProps[widgetType].includes(propertyPath)
|
||||
);
|
||||
}
|
||||
|
||||
// Checks if the input value is only one word
|
||||
export const shouldShowAutocompleteWithBindingBrackets = (
|
||||
editor: CodeMirror.Editor,
|
||||
) => {
|
||||
const editorMode = editor.getModeAt(editor.getCursor());
|
||||
|
||||
if (editorMode?.name === EditorModes.SQL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const value = editor.getValue();
|
||||
|
||||
// Do not show if the value is "/"
|
||||
if (value.startsWith("/") || value.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split the value by whitespace
|
||||
const stringSegments = value.split(/\s+/);
|
||||
|
||||
return stringSegments.length === 1;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,8 +64,9 @@ describe("hint helpers", () => {
|
|||
toCall: "closeHint" | "showHint";
|
||||
getLine?: string[];
|
||||
}
|
||||
|
||||
const cases: Case[] = [
|
||||
{ value: "ABC", cursor: { ch: 3, line: 0 }, toCall: "closeHint" },
|
||||
{ value: "ABC", cursor: { ch: 3, line: 0 }, toCall: "showHint" },
|
||||
{ value: "{{ }}", cursor: { ch: 3, line: 0 }, toCall: "showHint" },
|
||||
{
|
||||
value: '{ name: "{{}}" }',
|
||||
|
|
@ -91,7 +92,7 @@ describe("hint helpers", () => {
|
|||
{
|
||||
value: "{test(",
|
||||
cursor: { ch: 1, line: 0 },
|
||||
toCall: "closeHint",
|
||||
toCall: "showHint",
|
||||
},
|
||||
{
|
||||
value: "justanystring {{}}",
|
||||
|
|
@ -101,7 +102,7 @@ describe("hint helpers", () => {
|
|||
];
|
||||
|
||||
cases.forEach((testCase) => {
|
||||
MockCodemirrorEditor.getValue.mockReturnValueOnce(testCase.value);
|
||||
MockCodemirrorEditor.getValue.mockReturnValue(testCase.value);
|
||||
MockCodemirrorEditor.getCursor.mockReturnValue(testCase.cursor);
|
||||
|
||||
if (testCase.getLine) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { EditorModes } from "components/editorComponents/CodeEditor/EditorConfig
|
|||
import {
|
||||
checkIfCursorInsideBinding,
|
||||
isCursorOnEmptyToken,
|
||||
shouldShowAutocompleteWithBindingBrackets,
|
||||
} from "components/editorComponents/CodeEditor/codeEditorUtils";
|
||||
import { isEmpty, isString } from "lodash";
|
||||
import type { getAllDatasourceTableKeys } from "ee/selectors/entitiesSelector";
|
||||
|
|
@ -45,14 +46,14 @@ export const bindingHintHelper: HintHelper = (editor: CodeMirror.Editor) => {
|
|||
CodemirrorTernService.setEntityInformation(editor, entityInformation);
|
||||
}
|
||||
|
||||
let shouldShow = false;
|
||||
let shouldShow = true;
|
||||
|
||||
if (additionalData?.isJsEditor) {
|
||||
if (additionalData?.enableAIAssistance) {
|
||||
shouldShow = !isAISlashCommand(editor);
|
||||
} else {
|
||||
shouldShow = true;
|
||||
}
|
||||
} else if (shouldShowAutocompleteWithBindingBrackets(editor)) {
|
||||
shouldShow = true;
|
||||
} else {
|
||||
shouldShow = checkIfCursorInsideBinding(editor);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import { findIndex, isString } from "lodash";
|
||||
import { renderTernTooltipContent } from "./ternDocTooltip";
|
||||
import { checkIfCursorInsideBinding } from "components/editorComponents/CodeEditor/codeEditorUtils";
|
||||
|
||||
const bigDoc = 250;
|
||||
const cls = "CodeMirror-Tern-";
|
||||
|
|
@ -516,6 +517,8 @@ class CodeMirrorTernService {
|
|||
const lineValue = this.lineValue(doc);
|
||||
const cursor = cm.getCursor();
|
||||
const { extraChars } = this.getFocusedDocValueAndPos(doc);
|
||||
const fieldIsJSAction =
|
||||
this.fieldEntityInformation.entityType === ENTITY_TYPE.JSACTION;
|
||||
|
||||
let completions: Completion<TernCompletionResult>[] = [];
|
||||
let after = "";
|
||||
|
|
@ -550,6 +553,7 @@ class CodeMirrorTernService {
|
|||
|
||||
if (typeof completion === "string") continue;
|
||||
|
||||
const isCursorInsideBinding = checkIfCursorInsideBinding(cm);
|
||||
const isKeyword = isCustomKeywordType(completion);
|
||||
const className = typeToIcon(completion.type as string, isKeyword);
|
||||
const dataType = getDataType(completion.type as string);
|
||||
|
|
@ -589,6 +593,11 @@ class CodeMirrorTernService {
|
|||
isEntityName: isCompletionADataTreeEntityName,
|
||||
};
|
||||
|
||||
if (!isCursorInsideBinding && !fieldIsJSAction) {
|
||||
codeMirrorCompletion.displayText = `{{${codeMirrorCompletion.displayText}}}`;
|
||||
codeMirrorCompletion.text = `{{${codeMirrorCompletion.text}}}`;
|
||||
}
|
||||
|
||||
if (isKeyword) {
|
||||
codeMirrorCompletion.render = (
|
||||
element: HTMLElement,
|
||||
|
|
@ -630,16 +639,13 @@ class CodeMirrorTernService {
|
|||
completions.push(codeMirrorCompletion);
|
||||
}
|
||||
|
||||
const shouldComputeBestMatch =
|
||||
this.fieldEntityInformation.entityType !== ENTITY_TYPE.JSACTION;
|
||||
|
||||
completions = AutocompleteSorter.sort(
|
||||
completions,
|
||||
{ ...this.fieldEntityInformation, token },
|
||||
this.defEntityInformation.get(
|
||||
this.fieldEntityInformation.entityName || "",
|
||||
),
|
||||
shouldComputeBestMatch,
|
||||
!fieldIsJSAction,
|
||||
);
|
||||
const indexToBeSelected =
|
||||
completions.length && completions[0].isHeader ? 1 : 0;
|
||||
|
|
@ -747,13 +753,23 @@ class CodeMirrorTernService {
|
|||
libraryNamespace: selected.origin?.split("/")[1],
|
||||
});
|
||||
|
||||
// Check if the completion ends with parentheses () or closing brackets }}
|
||||
const hasParenthesis = selected.text.endsWith("()");
|
||||
const endsWithBindingBrackets = selected.text.endsWith("}}");
|
||||
|
||||
// Position cursor handling:
|
||||
// 1. For functions - place cursor between parentheses e.g. myFunction(|)
|
||||
// 2. For completions with }} - place cursor before }} e.g. {{Api1.data|}}
|
||||
if (selected.type === AutocompleteDataType.FUNCTION && hasParenthesis) {
|
||||
cm.setCursor({
|
||||
line: cm.getCursor().line,
|
||||
ch: cm.getCursor().ch - 1,
|
||||
});
|
||||
} else if (endsWithBindingBrackets) {
|
||||
cm.setCursor({
|
||||
line: cm.getCursor().line,
|
||||
ch: cm.getCursor().ch - 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1359,17 +1375,17 @@ class CodeMirrorTernService {
|
|||
const CodeMirror = getCodeMirrorNamespaceFromDoc(cm.getDoc());
|
||||
const inner = CodeMirror.innerMode(cm.getMode(), state);
|
||||
|
||||
if (inner.mode.name != "javascript") return false;
|
||||
if (inner.mode.name === "javascript") {
|
||||
const lex = inner.state.lexical;
|
||||
|
||||
const lex = inner.state.lexical;
|
||||
if (lex.info === "call") {
|
||||
const argPos = lex.pos || 0;
|
||||
const args = this.cachedArgHints?.type?.args || [];
|
||||
const arg = args[argPos];
|
||||
const argType = arg?.type;
|
||||
|
||||
if (lex.info === "call") {
|
||||
const argPos = lex.pos || 0;
|
||||
const args = this.cachedArgHints?.type?.args || [];
|
||||
const arg = args[argPos];
|
||||
const argType = arg?.type;
|
||||
|
||||
entityInformation.expectedType = getDataType(argType);
|
||||
entityInformation.expectedType = getDataType(argType);
|
||||
}
|
||||
}
|
||||
|
||||
this.fieldEntityInformation = entityInformation;
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ describe("Tern server", () => {
|
|||
MockCodemirrorEditor.getValue.mockReturnValueOnce(
|
||||
testCase.input.codeEditor.value,
|
||||
);
|
||||
MockCodemirrorEditor.getCursor.mockReturnValueOnce(
|
||||
MockCodemirrorEditor.getCursor.mockReturnValue(
|
||||
testCase.input.codeEditor.cursor,
|
||||
);
|
||||
MockCodemirrorEditor.getDoc.mockReturnValue(
|
||||
|
|
@ -689,6 +689,11 @@ describe("Tern server completion", () => {
|
|||
ch: 30,
|
||||
sticky: null,
|
||||
});
|
||||
|
||||
MockCodemirrorEditor.getValue.mockReturnValue(
|
||||
"\t\tconst users = await QueryMod",
|
||||
);
|
||||
|
||||
MockCodemirrorEditor.getTokenAt.mockResolvedValue(mockToken);
|
||||
CodemirrorTernService.fieldEntityInformation = fieldEntityInformation;
|
||||
CodemirrorTernService.entityDef = entityDef;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const MockCodemirrorEditor = {
|
|||
getDoc: jest.fn(),
|
||||
getTokenAt: jest.fn(),
|
||||
getMode: jest.fn(),
|
||||
getModeAt: jest.fn(),
|
||||
constructor: MockCodemirrorNamespace,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user