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);
|
_.entityExplorer.DragDropWidgetNVerify(_.draggableWidgets.JSONFORM);
|
||||||
_.propPane.EnterJSContext(
|
_.propPane.EnterJSContext(
|
||||||
"sourcedata",
|
"sourcedata",
|
||||||
JSON.stringify(schema),
|
JSON.stringify(schema) + " ",
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ describe(
|
||||||
entityExplorer.DragDropWidgetNVerify(draggableWidgets.JSONFORM, 300, 100);
|
entityExplorer.DragDropWidgetNVerify(draggableWidgets.JSONFORM, 300, 100);
|
||||||
propPane.EnterJSContext(
|
propPane.EnterJSContext(
|
||||||
"sourcedata",
|
"sourcedata",
|
||||||
JSON.stringify(schema),
|
JSON.stringify(schema) + " ",
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ describe(
|
||||||
);
|
);
|
||||||
|
|
||||||
// remove all table data
|
// remove all table data
|
||||||
propPane.UpdatePropertyFieldValue("Table data", `[]`);
|
propPane.UpdatePropertyFieldValue("Table data", `[] `);
|
||||||
|
|
||||||
// allow adding a row
|
// allow adding a row
|
||||||
propPane.TogglePropertyState("Allow adding a row", "On");
|
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 type { WidgetEntity, ActionEntity } from "ee/entities/DataTree/types";
|
||||||
import { trim } from "lodash";
|
import { trim } from "lodash";
|
||||||
import { getDynamicStringSegments } from "utils/DynamicBindingUtils";
|
import { getDynamicStringSegments } from "utils/DynamicBindingUtils";
|
||||||
import { EditorSize } from "./EditorConfig";
|
import { EditorModes, EditorSize } from "./EditorConfig";
|
||||||
import { SlashCommandMenuOnFocusWidgetProps } from "./constants";
|
import { SlashCommandMenuOnFocusWidgetProps } from "./constants";
|
||||||
|
|
||||||
// TODO: Fix this the next time the file is edited
|
export const removeNewLineChars = (inputValue: string) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const removeNewLineChars = (inputValue: any) => {
|
|
||||||
return inputValue && inputValue.replace(/(\r\n|\n|\r)/gm, "");
|
return inputValue && inputValue.replace(/(\r\n|\n|\r)/gm, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Fix this the next time the file is edited
|
export const getInputValue = (inputValue: unknown): string => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const getInputValue = (inputValue: any) => {
|
|
||||||
if (typeof inputValue === "object" || typeof inputValue === "boolean") {
|
if (typeof inputValue === "object" || typeof inputValue === "boolean") {
|
||||||
inputValue = JSON.stringify(inputValue, null, 2);
|
inputValue = JSON.stringify(inputValue, null, 2);
|
||||||
} else if (typeof inputValue === "number" || typeof inputValue === "string") {
|
} else if (typeof inputValue === "number" || typeof inputValue === "string") {
|
||||||
inputValue += "";
|
inputValue += "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return inputValue;
|
return String(inputValue || "");
|
||||||
};
|
};
|
||||||
const computeCursorIndex = (editor: CodeMirror.Editor) => {
|
const computeCursorIndex = (editor: CodeMirror.Editor) => {
|
||||||
const cursor = editor.getCursor();
|
const cursor = editor.getCursor();
|
||||||
|
|
@ -170,3 +166,26 @@ export function shouldShowSlashCommandMenu(
|
||||||
SlashCommandMenuOnFocusWidgetProps[widgetType].includes(propertyPath)
|
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";
|
toCall: "closeHint" | "showHint";
|
||||||
getLine?: string[];
|
getLine?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cases: Case[] = [
|
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: "{{ }}", cursor: { ch: 3, line: 0 }, toCall: "showHint" },
|
||||||
{
|
{
|
||||||
value: '{ name: "{{}}" }',
|
value: '{ name: "{{}}" }',
|
||||||
|
|
@ -91,7 +92,7 @@ describe("hint helpers", () => {
|
||||||
{
|
{
|
||||||
value: "{test(",
|
value: "{test(",
|
||||||
cursor: { ch: 1, line: 0 },
|
cursor: { ch: 1, line: 0 },
|
||||||
toCall: "closeHint",
|
toCall: "showHint",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "justanystring {{}}",
|
value: "justanystring {{}}",
|
||||||
|
|
@ -101,7 +102,7 @@ describe("hint helpers", () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
cases.forEach((testCase) => {
|
cases.forEach((testCase) => {
|
||||||
MockCodemirrorEditor.getValue.mockReturnValueOnce(testCase.value);
|
MockCodemirrorEditor.getValue.mockReturnValue(testCase.value);
|
||||||
MockCodemirrorEditor.getCursor.mockReturnValue(testCase.cursor);
|
MockCodemirrorEditor.getCursor.mockReturnValue(testCase.cursor);
|
||||||
|
|
||||||
if (testCase.getLine) {
|
if (testCase.getLine) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { EditorModes } from "components/editorComponents/CodeEditor/EditorConfig
|
||||||
import {
|
import {
|
||||||
checkIfCursorInsideBinding,
|
checkIfCursorInsideBinding,
|
||||||
isCursorOnEmptyToken,
|
isCursorOnEmptyToken,
|
||||||
|
shouldShowAutocompleteWithBindingBrackets,
|
||||||
} from "components/editorComponents/CodeEditor/codeEditorUtils";
|
} from "components/editorComponents/CodeEditor/codeEditorUtils";
|
||||||
import { isEmpty, isString } from "lodash";
|
import { isEmpty, isString } from "lodash";
|
||||||
import type { getAllDatasourceTableKeys } from "ee/selectors/entitiesSelector";
|
import type { getAllDatasourceTableKeys } from "ee/selectors/entitiesSelector";
|
||||||
|
|
@ -45,14 +46,14 @@ export const bindingHintHelper: HintHelper = (editor: CodeMirror.Editor) => {
|
||||||
CodemirrorTernService.setEntityInformation(editor, entityInformation);
|
CodemirrorTernService.setEntityInformation(editor, entityInformation);
|
||||||
}
|
}
|
||||||
|
|
||||||
let shouldShow = false;
|
let shouldShow = true;
|
||||||
|
|
||||||
if (additionalData?.isJsEditor) {
|
if (additionalData?.isJsEditor) {
|
||||||
if (additionalData?.enableAIAssistance) {
|
if (additionalData?.enableAIAssistance) {
|
||||||
shouldShow = !isAISlashCommand(editor);
|
shouldShow = !isAISlashCommand(editor);
|
||||||
} else {
|
|
||||||
shouldShow = true;
|
|
||||||
}
|
}
|
||||||
|
} else if (shouldShowAutocompleteWithBindingBrackets(editor)) {
|
||||||
|
shouldShow = true;
|
||||||
} else {
|
} else {
|
||||||
shouldShow = checkIfCursorInsideBinding(editor);
|
shouldShow = checkIfCursorInsideBinding(editor);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||||
import { findIndex, isString } from "lodash";
|
import { findIndex, isString } from "lodash";
|
||||||
import { renderTernTooltipContent } from "./ternDocTooltip";
|
import { renderTernTooltipContent } from "./ternDocTooltip";
|
||||||
|
import { checkIfCursorInsideBinding } from "components/editorComponents/CodeEditor/codeEditorUtils";
|
||||||
|
|
||||||
const bigDoc = 250;
|
const bigDoc = 250;
|
||||||
const cls = "CodeMirror-Tern-";
|
const cls = "CodeMirror-Tern-";
|
||||||
|
|
@ -516,6 +517,8 @@ class CodeMirrorTernService {
|
||||||
const lineValue = this.lineValue(doc);
|
const lineValue = this.lineValue(doc);
|
||||||
const cursor = cm.getCursor();
|
const cursor = cm.getCursor();
|
||||||
const { extraChars } = this.getFocusedDocValueAndPos(doc);
|
const { extraChars } = this.getFocusedDocValueAndPos(doc);
|
||||||
|
const fieldIsJSAction =
|
||||||
|
this.fieldEntityInformation.entityType === ENTITY_TYPE.JSACTION;
|
||||||
|
|
||||||
let completions: Completion<TernCompletionResult>[] = [];
|
let completions: Completion<TernCompletionResult>[] = [];
|
||||||
let after = "";
|
let after = "";
|
||||||
|
|
@ -550,6 +553,7 @@ class CodeMirrorTernService {
|
||||||
|
|
||||||
if (typeof completion === "string") continue;
|
if (typeof completion === "string") continue;
|
||||||
|
|
||||||
|
const isCursorInsideBinding = checkIfCursorInsideBinding(cm);
|
||||||
const isKeyword = isCustomKeywordType(completion);
|
const isKeyword = isCustomKeywordType(completion);
|
||||||
const className = typeToIcon(completion.type as string, isKeyword);
|
const className = typeToIcon(completion.type as string, isKeyword);
|
||||||
const dataType = getDataType(completion.type as string);
|
const dataType = getDataType(completion.type as string);
|
||||||
|
|
@ -589,6 +593,11 @@ class CodeMirrorTernService {
|
||||||
isEntityName: isCompletionADataTreeEntityName,
|
isEntityName: isCompletionADataTreeEntityName,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isCursorInsideBinding && !fieldIsJSAction) {
|
||||||
|
codeMirrorCompletion.displayText = `{{${codeMirrorCompletion.displayText}}}`;
|
||||||
|
codeMirrorCompletion.text = `{{${codeMirrorCompletion.text}}}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (isKeyword) {
|
if (isKeyword) {
|
||||||
codeMirrorCompletion.render = (
|
codeMirrorCompletion.render = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
|
|
@ -630,16 +639,13 @@ class CodeMirrorTernService {
|
||||||
completions.push(codeMirrorCompletion);
|
completions.push(codeMirrorCompletion);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldComputeBestMatch =
|
|
||||||
this.fieldEntityInformation.entityType !== ENTITY_TYPE.JSACTION;
|
|
||||||
|
|
||||||
completions = AutocompleteSorter.sort(
|
completions = AutocompleteSorter.sort(
|
||||||
completions,
|
completions,
|
||||||
{ ...this.fieldEntityInformation, token },
|
{ ...this.fieldEntityInformation, token },
|
||||||
this.defEntityInformation.get(
|
this.defEntityInformation.get(
|
||||||
this.fieldEntityInformation.entityName || "",
|
this.fieldEntityInformation.entityName || "",
|
||||||
),
|
),
|
||||||
shouldComputeBestMatch,
|
!fieldIsJSAction,
|
||||||
);
|
);
|
||||||
const indexToBeSelected =
|
const indexToBeSelected =
|
||||||
completions.length && completions[0].isHeader ? 1 : 0;
|
completions.length && completions[0].isHeader ? 1 : 0;
|
||||||
|
|
@ -747,13 +753,23 @@ class CodeMirrorTernService {
|
||||||
libraryNamespace: selected.origin?.split("/")[1],
|
libraryNamespace: selected.origin?.split("/")[1],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if the completion ends with parentheses () or closing brackets }}
|
||||||
const hasParenthesis = selected.text.endsWith("()");
|
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) {
|
if (selected.type === AutocompleteDataType.FUNCTION && hasParenthesis) {
|
||||||
cm.setCursor({
|
cm.setCursor({
|
||||||
line: cm.getCursor().line,
|
line: cm.getCursor().line,
|
||||||
ch: cm.getCursor().ch - 1,
|
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 CodeMirror = getCodeMirrorNamespaceFromDoc(cm.getDoc());
|
||||||
const inner = CodeMirror.innerMode(cm.getMode(), state);
|
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") {
|
entityInformation.expectedType = getDataType(argType);
|
||||||
const argPos = lex.pos || 0;
|
}
|
||||||
const args = this.cachedArgHints?.type?.args || [];
|
|
||||||
const arg = args[argPos];
|
|
||||||
const argType = arg?.type;
|
|
||||||
|
|
||||||
entityInformation.expectedType = getDataType(argType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fieldEntityInformation = entityInformation;
|
this.fieldEntityInformation = entityInformation;
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ describe("Tern server", () => {
|
||||||
MockCodemirrorEditor.getValue.mockReturnValueOnce(
|
MockCodemirrorEditor.getValue.mockReturnValueOnce(
|
||||||
testCase.input.codeEditor.value,
|
testCase.input.codeEditor.value,
|
||||||
);
|
);
|
||||||
MockCodemirrorEditor.getCursor.mockReturnValueOnce(
|
MockCodemirrorEditor.getCursor.mockReturnValue(
|
||||||
testCase.input.codeEditor.cursor,
|
testCase.input.codeEditor.cursor,
|
||||||
);
|
);
|
||||||
MockCodemirrorEditor.getDoc.mockReturnValue(
|
MockCodemirrorEditor.getDoc.mockReturnValue(
|
||||||
|
|
@ -689,6 +689,11 @@ describe("Tern server completion", () => {
|
||||||
ch: 30,
|
ch: 30,
|
||||||
sticky: null,
|
sticky: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
MockCodemirrorEditor.getValue.mockReturnValue(
|
||||||
|
"\t\tconst users = await QueryMod",
|
||||||
|
);
|
||||||
|
|
||||||
MockCodemirrorEditor.getTokenAt.mockResolvedValue(mockToken);
|
MockCodemirrorEditor.getTokenAt.mockResolvedValue(mockToken);
|
||||||
CodemirrorTernService.fieldEntityInformation = fieldEntityInformation;
|
CodemirrorTernService.fieldEntityInformation = fieldEntityInformation;
|
||||||
CodemirrorTernService.entityDef = entityDef;
|
CodemirrorTernService.entityDef = entityDef;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export const MockCodemirrorEditor = {
|
||||||
getDoc: jest.fn(),
|
getDoc: jest.fn(),
|
||||||
getTokenAt: jest.fn(),
|
getTokenAt: jest.fn(),
|
||||||
getMode: jest.fn(),
|
getMode: jest.fn(),
|
||||||
|
getModeAt: jest.fn(),
|
||||||
constructor: MockCodemirrorNamespace,
|
constructor: MockCodemirrorNamespace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user