diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/StoreValue_spec.ts b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/StoreValue_spec.ts index 7ec68fa811..0ac6a3891b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/StoreValue_spec.ts +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ActionExecution/StoreValue_spec.ts @@ -2,10 +2,10 @@ import { ObjectsRegistry } from "../../../../support/Objects/Registry"; const { AggregateHelper: agHelper, + + DeployMode: deployMode, EntityExplorer: ee, JSEditor: jsEditor, - CommonLocators: locator, - DeployMode: deployMode, PropertyPane: propPane, } = ObjectsRegistry; diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Autocomplete/Autocomplete_JS_spec.ts b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Autocomplete/Autocomplete_JS_spec.ts new file mode 100644 index 0000000000..8eaae35552 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Autocomplete/Autocomplete_JS_spec.ts @@ -0,0 +1,121 @@ +import { WIDGET } from "../../../../locators/WidgetLocators"; +import { ObjectsRegistry } from "../../../../support/Objects/Registry"; +const explorer = require("../../../../locators/explorerlocators.json"); + +const { CommonLocators, EntityExplorer, JSEditor: jsEditor } = ObjectsRegistry; + +const jsObjectBody = `export default { + myVar1: [], + myVar2: {}, + myFun1(){ + + }, + myFun2: async () => { + //use async-await or promises + } +}`; + +describe("Autocomplete tests", () => { + before(() => { + cy.get(explorer.addWidget).click(); + EntityExplorer.DragDropWidgetNVerify(WIDGET.BUTTON_GROUP_WIDGET, 300, 500); + }); + + it("1. ButtonGroup autocomplete & Eval shouldn't show up", () => { + // create js object + jsEditor.CreateJSObject(jsObjectBody, { + paste: true, + completeReplace: true, + toRun: false, + shouldCreateNewJSObj: true, + }); + + const lineNumber = 5; + cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click(); + + cy.get(CommonLocators._codeMirrorTextArea) + .focus() + .type(`ButtonGroup1.`); + + cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("groupButtons"); + + cy.get(CommonLocators._codeMirrorTextArea) + .focus() + .type(`groupButtons.`); + + cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("groupButton1"); + + cy.get(CommonLocators._codeMirrorTextArea).focus().type(` + eval`); + + cy.get(`.CodeMirror-hints > :nth-child(1)`).should( + "not.have.value", + "eval()", + ); + }); + + it("2. Local variables autocompletion support", () => { + // create js object + jsEditor.CreateJSObject(jsObjectBody, { + paste: true, + completeReplace: true, + toRun: false, + shouldCreateNewJSObj: true, + }); + + const lineNumber = 5; + + const array = [ + { label: "a", value: "b" }, + { label: "a", value: "b" }, + ]; + + const codeToType = ` + const arr = ${JSON.stringify(array)}; + + arr.map(callBack) + `; + + // component re-render cause DOM element of cy.get to lost + // added wait to finish re-render before cy.get + cy.wait(100); + + cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click(); + + cy.get(CommonLocators._codeMirrorTextArea) + .focus() + .type(`${codeToType}`, { parseSpecialCharSequences: false }) + .type(`{upArrow}{upArrow}`) + .type(`const callBack = (item) => item.l`); + + cy.get(`.CodeMirror-hints > :nth-child(1)`).contains("label"); + + cy.get(CommonLocators._codeMirrorTextArea) + .focus() + .type(`label`); + }); + + it("3. JSObject this. autocomplete", () => { + // create js object + jsEditor.CreateJSObject(jsObjectBody, { + paste: true, + completeReplace: true, + toRun: false, + shouldCreateNewJSObj: true, + }); + + const lineNumber = 5; + + const codeToType = "this."; + + cy.get(`:nth-child(${lineNumber}) > .CodeMirror-line`).click(); + + cy.get(CommonLocators._codeMirrorTextArea) + .focus() + .type(`${codeToType}`); + + ["myFun2()", "myVar1", "myVar2"].forEach((element, index) => { + cy.get(`.CodeMirror-hints > :nth-child(${index + 1})`).contains(element); + }); + }); +}); diff --git a/app/client/cypress/locators/WidgetLocators.ts b/app/client/cypress/locators/WidgetLocators.ts index ebedf18360..0b95f2cef3 100644 --- a/app/client/cypress/locators/WidgetLocators.ts +++ b/app/client/cypress/locators/WidgetLocators.ts @@ -5,6 +5,7 @@ export const WIDGET = { CURRENCY_INPUT_WIDGET: "currencyinputwidget", BUTTON_WIDGET: "buttonwidget", MULTISELECT_WIDGET: "multiselectwidgetv2", + BUTTON_GROUP_WIDGET: "buttongroupwidget", TREESELECT_WIDGET: "singleselecttreewidget", TAB: "tabswidget", TABLE: "tablewidgetv2", diff --git a/app/client/cypress/support/Pages/JSEditor.ts b/app/client/cypress/support/Pages/JSEditor.ts index cbd6105db9..5c3cc56271 100644 --- a/app/client/cypress/support/Pages/JSEditor.ts +++ b/app/client/cypress/support/Pages/JSEditor.ts @@ -5,12 +5,14 @@ export interface ICreateJSObjectOptions { completeReplace: boolean; toRun: boolean; shouldCreateNewJSObj: boolean; + lineNumber?: number; } const DEFAULT_CREATE_JS_OBJECT_OPTIONS = { paste: true, completeReplace: false, toRun: true, shouldCreateNewJSObj: true, + lineNumber: 4, }; export class JSEditor { @@ -118,30 +120,29 @@ export class JSEditor { JSCode: string, options: ICreateJSObjectOptions = DEFAULT_CREATE_JS_OBJECT_OPTIONS, ) { - const { completeReplace, paste, shouldCreateNewJSObj, toRun } = options; + const { + completeReplace, + lineNumber = 4, + paste, + shouldCreateNewJSObj, + toRun, + } = options; shouldCreateNewJSObj && this.NavigateToNewJSEditor(); if (!completeReplace) { + const downKeys = Array.from(new Array(lineNumber), () => "{downarrow}") + .toString() + .replaceAll(",", ""); cy.get(this.locator._codeMirrorTextArea) .first() .focus() - .type("{downarrow}{downarrow}{downarrow}{downarrow} "); + .type(`${downKeys} `); } else { cy.get(this.locator._codeMirrorTextArea) .first() .focus() .type(this.selectAllJSObjectContentShortcut) .type("{backspace}", { force: true }); - - // .type("{uparrow}", { force: true }) - // .type("{ctrl}{shift}{downarrow}", { force: true }) - // .type("{del}",{ force: true }); - - // cy.get(this.locator._codthis.eeditorTarget).contains('export').click().closest(this.locator._codthis.eeditorTarget) - // .type("{uparrow}", { force: true }) - // .type("{ctrl}{shift}{downarrow}", { force: true }) - // .type("{backspace}",{ force: true }); - //.type("{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}{downarrow} ") } cy.get(this.locator._codeMirrorTextArea) diff --git a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts index 102be2dadc..4bedbf9521 100644 --- a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts +++ b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts @@ -45,9 +45,10 @@ export const EditorThemes: Record = { export type FieldEntityInformation = { entityName?: string; expectedType?: AutocompleteDataType; - entityType?: ENTITY_TYPE.ACTION | ENTITY_TYPE.WIDGET | ENTITY_TYPE.JSACTION; + entityType?: ENTITY_TYPE; entityId?: string; propertyPath?: string; + blockCompletions?: Array<{ parentPath: string; subPath: string }>; }; export type HintHelper = ( diff --git a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts index 1868b3cbc6..33a345db8c 100644 --- a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts +++ b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts @@ -28,8 +28,20 @@ export const bindingHint: HintHelper = (editor, dataTree, customDataTree) => { }, }); return { - showHint: (editor: CodeMirror.Editor, entityInformation): boolean => { - TernServer.setEntityInformation(entityInformation); + showHint: ( + editor: CodeMirror.Editor, + entityInformation, + additionalData, + ): boolean => { + if (additionalData && additionalData.blockCompletions) { + TernServer.setEntityInformation({ + ...entityInformation, + blockCompletions: additionalData.blockCompletions, + }); + } else { + TernServer.setEntityInformation(entityInformation); + } + const entityType = entityInformation?.entityType; let shouldShow = false; if (entityType === ENTITY_TYPE.JSACTION) { diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index 79dbb566b7..a0813039b4 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -101,17 +101,8 @@ import { getMoveCursorLeftKey } from "./utils/cursorLeftMovement"; import { interactionAnalyticsEvent } from "utils/AppsmithUtils"; import { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator"; -interface ReduxStateProps { - datasources: any; - dynamicData: DataTree; - pluginIdToImageLocation: Record; - recentEntities: string[]; -} - -interface ReduxDispatchProps { - executeCommand: (payload: any) => void; - startingEntityUpdation: () => void; -} +type ReduxStateProps = ReturnType; +type ReduxDispatchProps = ReturnType; export type CodeEditorExpected = { type: string; @@ -141,6 +132,7 @@ export type EditorStyleProps = { evaluationSubstitutionType?: EvaluationSubstitutionType; popperPlacement?: Placement; popperZIndex?: Indices; + blockCompletions?: FieldEntityInformation["blockCompletions"]; }; /** * line => Line to which the gutter is added @@ -190,9 +182,7 @@ export type EditorProps = EditorStyleProps & customGutter?: CodeEditorGutter; }; -type Props = ReduxStateProps & - EditorProps & - ReduxDispatchProps & { dispatch?: () => void }; +interface Props extends ReduxStateProps, EditorProps, ReduxDispatchProps {} type State = { isFocused: boolean; @@ -525,12 +515,16 @@ class CodeEditor extends Component { handleEditorFocus = (cm: CodeMirror.Editor) => { this.setState({ isFocused: true }); + if (!cm.state.completionActive) { - const entityInformation: FieldEntityInformation = this.getEntityInformation(); + const entityInformation = this.getEntityInformation(); + const { blockCompletions } = this.props; this.hinters .filter((hinter) => hinter.fireOnFocus) .forEach( - (hinter) => hinter.showHint && hinter.showHint(cm, entityInformation), + (hinter) => + hinter.showHint && + hinter.showHint(cm, entityInformation, blockCompletions), ); } }; @@ -656,10 +650,12 @@ class CodeEditor extends Component { handleAutocompleteVisibility = (cm: CodeMirror.Editor) => { if (!this.state.isFocused) return; - const entityInformation: FieldEntityInformation = this.getEntityInformation(); + const entityInformation = this.getEntityInformation(); + const { blockCompletions } = this.props; let hinterOpen = false; for (let i = 0; i < this.hinters.length; i++) { hinterOpen = this.hinters[i].showHint(cm, entityInformation, { + blockCompletions, datasources: this.props.datasources.list, pluginIdToImageLocation: this.props.pluginIdToImageLocation, recentEntities: this.props.recentEntities, @@ -957,14 +953,14 @@ class CodeEditor extends Component { } } -const mapStateToProps = (state: AppState): ReduxStateProps => ({ +const mapStateToProps = (state: AppState) => ({ dynamicData: getDataTreeForAutocomplete(state), datasources: state.entities.datasources, pluginIdToImageLocation: getPluginIdToImageLocation(state), recentEntities: getRecentEntityIds(state), }); -const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({ +const mapDispatchToProps = (dispatch: any) => ({ executeCommand: (payload: SlashCommandPayload) => dispatch(executeCommandAction(payload)), startingEntityUpdation: () => dispatch(startingEntityUpdation()), diff --git a/app/client/src/constants/defs/ecmascript.json b/app/client/src/constants/defs/ecmascript.json index 07bac36706..fcb2b44d21 100644 --- a/app/client/src/constants/defs/ecmascript.json +++ b/app/client/src/constants/defs/ecmascript.json @@ -1227,11 +1227,6 @@ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/isFinite", "!doc": "Determines whether the passed value is a finite number." }, - "eval": { - "!type": "fn(code: string) -> ?", - "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/eval", - "!doc": "Evaluates JavaScript code represented as a string." - }, "encodeURI": { "!type": "fn(uri: string) -> string", "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURI", diff --git a/app/client/src/pages/Editor/JSEditor/Form.tsx b/app/client/src/pages/Editor/JSEditor/Form.tsx index 61bd2e9dfb..b4a307b557 100644 --- a/app/client/src/pages/Editor/JSEditor/Form.tsx +++ b/app/client/src/pages/Editor/JSEditor/Form.tsx @@ -181,6 +181,24 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) { } setSelectedJSActionOption(getJSActionOption(activeJSAction, jsActions)); }, [parseErrors, jsActions, activeJSActionId]); + + const blockCompletions = useMemo(() => { + if (selectedJSActionOption.label) { + const funcName = `${selectedJSActionOption.label}()`; + return [ + { + parentPath: "this", + subPath: funcName, + }, + { + parentPath: currentJSCollection.name, + subPath: funcName, + }, + ]; + } + return []; + }, [selectedJSActionOption.label, currentJSCollection.name]); + return ( @@ -224,6 +242,7 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) { title: "Code", panelComponent: ( { + return { + "!doc": + "The Button group widget represents a set of buttons in a group. Group can have simple buttons or menu buttons with drop-down items.", + "!url": "https://docs.appsmith.com/widget-reference/button-group", + groupButtons: generateTypeDef(widget.groupButtons), + }; + }, DATE_PICKER_WIDGET: { "!doc": "Datepicker is used to capture the date and time from a user. It can be used to filter data base on the input date range as well as to capture personal information such as date of birth", @@ -302,7 +311,7 @@ export const entityDefinitions = { "Radio widget lets the user choose only one option from a predefined set of options. It is quite similar to a SingleSelect Dropdown in its functionality", "!url": "https://docs.appsmith.com/widget-reference/radio", isVisible: isVisible, - options: "[dropdownOption]", + options: "[$__dropdownOption__$]", selectedOptionValue: "string", isRequired: "bool", }, @@ -324,10 +333,13 @@ export const entityDefinitions = { "Chart widget is used to view the graphical representation of your data. Chart is the go-to widget for your data visualisation needs.", "!url": "https://docs.appsmith.com/widget-reference/chart", isVisible: isVisible, - chartData: "chartData", + chartData: { + seriesName: "string", + data: "[$__chartDataPoint__$]", + }, xAxisName: "string", yAxisName: "string", - selectedDataPoint: "chartDataPoint", + selectedDataPoint: "$__chartDataPoint__$", }, FORM_WIDGET: (widget: any) => ({ "!doc": @@ -348,16 +360,25 @@ export const entityDefinitions = { }, MAP_WIDGET: { isVisible: isVisible, - center: "latLong", - markers: "[mapMarker]", - selectedMarker: "mapMarker", + center: { + lat: "number", + long: "number", + title: "string", + }, + markers: "[$__mapMarker__$]", + selectedMarker: { + lat: "number", + long: "number", + title: "string", + description: "string", + }, }, FILE_PICKER_WIDGET: { "!doc": "Filepicker widget is used to allow users to upload files from their local machines to any cloud storage via API. Cloudinary and Amazon S3 have simple APIs for cloud storage uploads", "!url": "https://docs.appsmith.com/widget-reference/filepicker", isVisible: isVisible, - files: "[file]", + files: "[$__file__$]", isDisabled: "bool", }, FILE_PICKER_WIDGET_V2: { @@ -365,7 +386,7 @@ export const entityDefinitions = { "Filepicker widget is used to allow users to upload files from their local machines to any cloud storage via API. Cloudinary and Amazon S3 have simple APIs for cloud storage uploads", "!url": "https://docs.appsmith.com/widget-reference/filepicker", isVisible: isVisible, - files: "[file]", + files: "[$__file__$]", isDisabled: "bool", }, LIST_WIDGET: (widget: any) => ({ @@ -436,7 +457,7 @@ export const entityDefinitions = { }, isDisabled: "bool", isValid: "bool", - options: "[dropdownOption]", + options: "[$__dropdownOption__$]", }, MULTI_SELECT_TREE_WIDGET: { "!doc": @@ -455,7 +476,7 @@ export const entityDefinitions = { }, isDisabled: "bool", isValid: "bool", - options: "[dropdownOption]", + options: "[$__dropdownOption__$]", }, ICON_BUTTON_WIDGET: { "!doc": @@ -470,7 +491,7 @@ export const entityDefinitions = { isVisible: isVisible, isDisabled: "bool", isValid: "bool", - options: "[dropdownOption]", + options: "[$__dropdownOption__$]", selectedValues: "[string]", }, STATBOX_WIDGET: { @@ -515,7 +536,13 @@ export const entityDefinitions = { "Map Chart widget shows the graphical representation of your data on the map.", "!url": "https://docs.appsmith.com/widget-reference/map-chart", isVisible: isVisible, - selectedDataPoint: "mapChartDataPoint", + selectedDataPoint: { + id: "string", + label: "string", + originalId: "string", + shortLabel: "string", + value: "number", + }, }, INPUT_WIDGET_V2: { "!doc": @@ -607,46 +634,31 @@ export const entityDefinitions = { }, }; +/* + $__name__$ is just to reduce occurrences of global def showing up in auto completion for user as `$` is less commonly used as entityName/ + + GLOBAL_DEFS are maintained to support definition for array of objects which currently aren't supported by our generateTypeDef. +*/ export const GLOBAL_DEFS = { - dropdownOption: { + $__dropdownOption__$: { label: "string", value: "string", }, - tabs: { - id: "string", - label: "string", - }, - chartDataPoint: { + $__chartDataPoint__$: { x: "string", y: "string", }, - chartData: { - seriesName: "string", - data: "[chartDataPoint]", - }, - latLong: { - lat: "number", - long: "number", - title: "string", - }, - mapMarker: { - lat: "number", - long: "number", - title: "string", - description: "string", - }, - file: { + $__file__$: { data: "string", dataFormat: "string", name: "text", type: "file", }, - mapChartDataPoint: { - id: "string", - label: "string", - originalId: "string", - shortLabel: "string", - value: "number", + $__mapMarker__$: { + lat: "number", + long: "number", + title: "string", + description: "string", }, }; diff --git a/app/client/src/utils/autocomplete/TernServer.test.ts b/app/client/src/utils/autocomplete/TernServer.test.ts index 882ada940b..08a20e62cc 100644 --- a/app/client/src/utils/autocomplete/TernServer.test.ts +++ b/app/client/src/utils/autocomplete/TernServer.test.ts @@ -62,6 +62,7 @@ describe("Tern server", () => { getCursor: () => ({ ch: 0, line: 0 }), getLine: () => "{{Api.}}", somethingSelected: () => false, + getValue: () => "", } as unknown) as CodeMirror.Doc, changed: null, }, @@ -71,9 +72,10 @@ describe("Tern server", () => { input: { name: "test", doc: ({ - getCursor: () => ({ ch: 0, line: 1 }), + getCursor: () => ({ ch: 0, line: 0 }), getLine: () => "{{Api.}}", somethingSelected: () => false, + getValue: () => "", } as unknown) as CodeMirror.Doc, changed: null, }, @@ -83,13 +85,14 @@ describe("Tern server", () => { input: { name: "test", doc: ({ - getCursor: () => ({ ch: 3, line: 1 }), + getCursor: () => ({ ch: 3, line: 0 }), getLine: () => "g {{Api.}}", somethingSelected: () => false, + getValue: () => "", } as unknown) as CodeMirror.Doc, changed: null, }, - expectedOutput: { ch: 1, line: 0 }, + expectedOutput: { ch: 3, line: 0 }, }, ]; @@ -126,20 +129,20 @@ describe("Tern server", () => { input: { codeEditor: { value: "\n {{}}", - cursor: { ch: 3, line: 1 }, + cursor: { ch: 3, line: 0 }, doc: ({ - getCursor: () => ({ ch: 3, line: 1 }), + getCursor: () => ({ ch: 3, line: 0 }), getLine: () => " {{}}", somethingSelected: () => false, } as unknown) as CodeMirror.Doc, }, requestCallbackData: { completions: [{ name: "Api1" }], - start: { ch: 2, line: 1 }, - end: { ch: 6, line: 1 }, + start: { ch: 2, line: 0 }, + end: { ch: 6, line: 0 }, }, }, - expectedOutput: { ch: 3, line: 1 }, + expectedOutput: { ch: 3, line: 0 }, }, ]; diff --git a/app/client/src/utils/autocomplete/TernServer.ts b/app/client/src/utils/autocomplete/TernServer.ts index ab286da065..810be7922e 100644 --- a/app/client/src/utils/autocomplete/TernServer.ts +++ b/app/client/src/utils/autocomplete/TernServer.ts @@ -78,6 +78,26 @@ type ArgHints = { doc: CodeMirror.Doc; }; +type RequestQuery = { + type: string; + types?: boolean; + docs?: boolean; + urls?: boolean; + origins?: boolean; + caseInsensitive?: boolean; + preferFunction?: boolean; + end?: CodeMirror.Position; + guess?: boolean; + inLiteral?: boolean; + fullDocs?: any; + lineCharPositions?: any; + start?: any; + file?: any; + includeKeywords?: boolean; + depth?: number; + sort?: boolean; +}; + export type DataTreeDefEntityInformation = { type: ENTITY_TYPE; subType: string; @@ -341,23 +361,13 @@ class TernServer { request( cm: CodeMirror.Editor, - query: { - type: string; - types?: boolean; - docs?: boolean; - urls?: boolean; - origins?: boolean; - caseInsensitive?: boolean; - preferFunction?: boolean; - end?: CodeMirror.Position; - guess?: boolean; - inLiteral?: boolean; - }, + query: RequestQuery | string, callbackFn: (error: any, data: any) => void, pos?: CodeMirror.Position, ) { const doc = this.findDoc(cm.getDoc()); const request = this.buildRequest(doc, query, pos); + // @ts-expect-error: Types are not available this.server.request(request, callbackFn); } @@ -389,55 +399,28 @@ class TernServer { buildRequest( doc: TernDoc, - query: { - type?: string; - types?: boolean; - docs?: boolean; - urls?: boolean; - origins?: boolean; - fullDocs?: any; - lineCharPositions?: any; - end?: any; - start?: any; - file?: any; - includeKeywords?: boolean; - inLiteral?: boolean; - depth?: number; - sort?: boolean; - }, + query: Partial | string, pos?: CodeMirror.Position, ) { const files = []; let offsetLines = 0; + if (typeof query == "string") query = { type: query }; const allowFragments = !query.fullDocs; if (!allowFragments) delete query.fullDocs; query.lineCharPositions = true; query.includeKeywords = true; query.depth = 0; query.sort = true; - if (!query.end) { - const lineValue = this.lineValue(doc); - const focusedValue = this.getFocusedDynamicValue(doc); - const index = lineValue.indexOf(focusedValue); - - const positions = pos || doc.doc.getCursor("end"); - const queryChPosition = positions.ch - index; - - query.end = { - ...positions, - line: 0, - ch: queryChPosition, - }; - - if (doc.doc.somethingSelected()) { - query.start = doc.doc.getCursor("start"); - } + if (query.end == null) { + query.end = pos || doc.doc.getCursor("end"); + if (doc.doc.somethingSelected()) query.start = doc.doc.getCursor("start"); } const startPos = query.start || query.end; + if (doc.changed) { if ( doc.doc.lineCount() > bigDoc && - allowFragments && + allowFragments !== false && doc.changed.to - doc.changed.from < 100 && doc.changed.from <= startPos.line && doc.changed.to > query.end.line @@ -445,29 +428,36 @@ class TernServer { files.push(this.getFragmentAround(doc, startPos, query.end)); query.file = "#0"; offsetLines = files[0].offsetLines; - if (query.start) { + if (query.start != null) query.start = Pos(query.start.line - -offsetLines, query.start.ch); - } query.end = Pos(query.end.line - offsetLines, query.end.ch); } else { files.push({ type: "full", name: doc.name, - text: this.getFocusedDynamicValue(doc), + text: this.docValue(doc), }); query.file = doc.name; doc.changed = null; } } else { query.file = doc.name; + // this code is different from tern.js code + // we noticed error `TernError: file doesn't contain line x` + // which was due to file not being present for the case when a codeEditor is opened and 1st character is typed + files.push({ + type: "full", + name: doc.name, + text: this.docValue(doc), + }); } for (const name in this.docs) { const cur = this.docs[name]; - if (cur.changed && cur !== doc) { + if (cur.changed && (cur != doc || cur.name != doc.name)) { files.push({ type: "full", name: cur.name, - text: this.getFocusedDynamicValue(cur), + text: this.docValue(cur), }); cur.changed = null; } diff --git a/app/client/src/utils/autocomplete/customTreeTypeDefCreator.ts b/app/client/src/utils/autocomplete/customTreeTypeDefCreator.ts index be9c9bd8c0..c3d4df9fc0 100644 --- a/app/client/src/utils/autocomplete/customTreeTypeDefCreator.ts +++ b/app/client/src/utils/autocomplete/customTreeTypeDefCreator.ts @@ -1,3 +1,4 @@ +import { Def } from "tern"; import { TruthyPrimitiveTypes } from "utils/TypeHelpers"; import { generateTypeDef } from "./dataTreeTypeDefCreator"; @@ -11,7 +12,7 @@ export type AdditionalDynamicDataTree = Record< export const customTreeTypeDefCreator = ( dataTree: AdditionalDynamicDataTree, ) => { - const def: any = { + const def: Def = { "!name": "customDataTree", }; Object.keys(dataTree).forEach((entityName) => { diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts index cf0ddd7c34..d5cafa9047 100644 --- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts +++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts @@ -2,6 +2,7 @@ import { generateTypeDef, dataTreeTypeDefCreator, flattenDef, + getFunctionsArgsType, } from "utils/autocomplete/dataTreeTypeDefCreator"; import { DataTreeWidget, @@ -121,3 +122,49 @@ describe("dataTreeTypeDefCreator", () => { expect(value).toStrictEqual(expected); }); }); + +describe("getFunctionsArgsType", () => { + const testCases = { + testCase1: { + arguments: [ + { name: "a", value: undefined }, + { name: "b", value: undefined }, + { name: "c", value: undefined }, + { name: "d", value: undefined }, + { name: "", value: undefined }, + ], + expectedOutput: "fn(a: ?, b: ?, c: ?, d: ?)", + }, + testCase2: { + arguments: [], + expectedOutput: "fn()", + }, + testCase3: { + arguments: [ + { name: "a", value: undefined }, + { name: "b", value: undefined }, + { name: "", value: undefined }, + { name: "", value: undefined }, + ], + expectedOutput: "fn(a: ?, b: ?)", + }, + }; + + it("function with 4 args", () => { + expect(getFunctionsArgsType(testCases.testCase1.arguments)).toEqual( + testCases.testCase1.expectedOutput, + ); + }); + + it("function with no args", () => { + expect(getFunctionsArgsType(testCases.testCase2.arguments)).toEqual( + testCases.testCase2.expectedOutput, + ); + }); + + it("function with 2 args", () => { + expect(getFunctionsArgsType(testCases.testCase3.arguments)).toEqual( + testCases.testCase3.expectedOutput, + ); + }); +}); diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts index f66a8cd6f5..b7f9a685bf 100644 --- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts +++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts @@ -1,9 +1,5 @@ -import { - DataTree, - ENTITY_TYPE, - MetaArgs, -} from "entities/DataTree/dataTreeFactory"; -import _ from "lodash"; +import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import { get, isFunction } from "lodash"; import { entityDefinitions } from "utils/autocomplete/EntityDefinitions"; import { getType, Types } from "utils/TypeHelpers"; import { Def } from "tern"; @@ -15,9 +11,11 @@ import { isWidget, } from "workers/evaluationUtils"; import { DataTreeDefEntityInformation } from "utils/autocomplete/TernServer"; +import { Variable } from "entities/JSCollection"; + // When there is a complex data type, we store it in extra def and refer to it // in the def -let extraDefs: any = {}; +let extraDefs: Def = {}; // Def names are encoded with information about the entity // This so that we have more info about them // when sorting results in autocomplete @@ -28,16 +26,17 @@ export const dataTreeTypeDefCreator = ( dataTree: DataTree, isJSEditorEnabled: boolean, ): { def: Def; entityInfo: Map } => { - const def: any = { + const def: Def = { "!name": "DATA_TREE", }; const entityMap: Map = new Map(); + Object.entries(dataTree).forEach(([entityName, entity]) => { if (isWidget(entity)) { const widgetType = entity.type; if (widgetType in entityDefinitions) { - const definition = _.get(entityDefinitions, widgetType); - if (_.isFunction(definition)) { + const definition = get(entityDefinitions, widgetType); + if (isFunction(definition)) { def[entityName] = definition(entity); } else { def[entityName] = definition; @@ -49,7 +48,7 @@ export const dataTreeTypeDefCreator = ( }); } } else if (isAction(entity)) { - def[entityName] = (entityDefinitions.ACTION as any)(entity); + def[entityName] = entityDefinitions.ACTION(entity); flattenDef(def, entityName); entityMap.set(entityName, { type: ENTITY_TYPE.ACTION, @@ -62,20 +61,27 @@ export const dataTreeTypeDefCreator = ( subType: ENTITY_TYPE.APPSMITH, }); } else if (isJSAction(entity) && isJSEditorEnabled) { - const metaObj: Record = entity.meta; - const jsOptions: Record = {}; + const metaObj = entity.meta; + const jsProperty: Def = {}; + for (const key in metaObj) { - jsOptions[key] = - "fn(onSuccess: fn() -> void, onError: fn() -> void) -> void"; + // const jsFunctionObj = metaObj[key]; + // const { arguments: args } = jsFunctionObj; + // const argsTypeString = getFunctionsArgsType(args); + // As we don't show args we avoid to get args def of function + // we will also need to check performance implications here + + const argsTypeString = getFunctionsArgsType([]); + jsProperty[key] = argsTypeString; } for (let i = 0; i < entity.variables.length; i++) { const varKey = entity.variables[i]; const varValue = entity[varKey]; - jsOptions[varKey] = generateTypeDef(varValue); + jsProperty[varKey] = generateTypeDef(varValue); } - def[entityName] = jsOptions; + def[entityName] = jsProperty; flattenDef(def, entityName); entityMap.set(entityName, { type: ENTITY_TYPE.JSACTION, @@ -87,12 +93,11 @@ export const dataTreeTypeDefCreator = ( extraDefs = {}; } }); + return { def, entityInfo: entityMap }; }; -export function generateTypeDef( - obj: any, -): string | Record> { +export function generateTypeDef(obj: any): string | Def { const type = getType(obj); switch (type) { case Types.ARRAY: { @@ -100,7 +105,7 @@ export function generateTypeDef( return `[${arrayType}]`; } case Types.OBJECT: { - const objType: Record> = {}; + const objType: Def = {}; Object.keys(obj).forEach((k) => { objType[k] = generateTypeDef(obj[k]); }); @@ -138,3 +143,32 @@ export const flattenDef = (def: Def, entityName: string): Def => { } return flattenedDef; }; + +const VALID_VARIABLE_NAME_REGEX = /^([a-zA-Z_$][a-zA-Z\d_$]*)$/; + +const isValidVariableName = (variableName: string) => + VALID_VARIABLE_NAME_REGEX.test(variableName); + +export const getFunctionsArgsType = (args: Variable[]): string => { + // skip same name args to avoiding creating invalid type + const argNames = new Set(); + // skip invalid args name + args.forEach((arg) => { + if (arg.name && isValidVariableName(arg.name)) argNames.add(arg.name); + }); + const argNamesArray = [...argNames]; + const argsTypeString = argNamesArray.reduce( + (accumulatedArgType, argName, currentIndex) => { + switch (currentIndex) { + case 0: + return `${argName}: ?`; + case 1: + return `${accumulatedArgType}, ${argName}: ?`; + default: + return `${accumulatedArgType}, ${argName}: ?`; + } + }, + argNamesArray[0], + ); + return argsTypeString ? `fn(${argsTypeString})` : `fn()`; +}; diff --git a/app/client/src/workers/DataTreeEvaluator/index.ts b/app/client/src/workers/DataTreeEvaluator/index.ts index 0a3474d807..304c1e59f1 100644 --- a/app/client/src/workers/DataTreeEvaluator/index.ts +++ b/app/client/src/workers/DataTreeEvaluator/index.ts @@ -84,7 +84,7 @@ import { getUpdatedLocalUnEvalTreeAfterJSUpdates, parseJSActions, } from "workers/JSObject"; -import { lintTree } from "workers/Lint"; +import { lintTree } from "workers/Lint/index"; export default class DataTreeEvaluator { dependencyMap: DependencyMap = {}; @@ -808,6 +808,8 @@ export default class DataTreeEvaluator { entityType = entity.type; } else if (entity && isAction(entity)) { entityType = entity.pluginType; + } else if (entity && isJSAction(entity)) { + entityType = entity.ENTITY_TYPE; } this.errors.push({ type: EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR,