fix: Improve Autocomplete for local vars, JSObject & ButtonGroup Widget

Add Autocomplete support for
- local variables 
- JSObjects
- ButtonGroupWidget
Remove Autocomplete suggestion for
- `eval`
-  undefined global values like `tabs`
This commit is contained in:
Rishabh Rathod 2022-08-11 15:29:38 +05:30 committed by GitHub
parent 1a6936435d
commit e255593e28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 430 additions and 167 deletions

View File

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

View File

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

View File

@ -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",

View File

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

View File

@ -45,9 +45,10 @@ export const EditorThemes: Record<EditorTheme, string> = {
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 = (

View File

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

View File

@ -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<string, string>;
recentEntities: string[];
}
interface ReduxDispatchProps {
executeCommand: (payload: any) => void;
startingEntityUpdation: () => void;
}
type ReduxStateProps = ReturnType<typeof mapStateToProps>;
type ReduxDispatchProps = ReturnType<typeof mapDispatchToProps>;
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<Props, State> {
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<Props, State> {
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<Props, State> {
}
}
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()),

View File

@ -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",

View File

@ -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 (
<FormWrapper>
<JSObjectHotKeys runActiveJSFunction={handleRunAction}>
@ -224,6 +242,7 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) {
title: "Code",
panelComponent: (
<CodeEditor
blockCompletions={blockCompletions}
className={"js-editor"}
customGutter={JSGutters}
dataTreePath={`${currentJSCollection.name}.body`}

View File

@ -43,6 +43,33 @@ export class AndRule implements AutocompleteRule {
}
}
/**
* Set score to -Infinity for paths to be blocked from autocompletion
* Max score - 0
* Min score - -Infinity
*/
class BlockSuggestionsRule implements AutocompleteRule {
static threshold = -Infinity;
computeScore(completion: Completion): number {
let score = 0;
const { currentFieldInfo } = AutocompleteSorter;
const { blockCompletions } = currentFieldInfo;
if (blockCompletions) {
for (let index = 0; index < blockCompletions.length; index++) {
const { subPath } = blockCompletions[index];
if (completion.text === subPath) {
score = BlockSuggestionsRule.threshold;
break;
}
}
}
return score;
}
}
/**
* Set score to -Infinity for suggestions like Table1.selectedRow.address
* Max score - 0
@ -252,6 +279,7 @@ export class ScoredCompletion {
new DataTreeFunctionRule(),
new JSLibraryRule(),
new GlobalJSRule(),
new BlockSuggestionsRule(),
];
completion: Completion;

View File

@ -6,6 +6,7 @@ import {
import _ from "lodash";
import { EVALUATION_PATH } from "utils/DynamicBindingUtils";
import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer";
import { ButtonGroupWidgetProps } from "widgets/ButtonGroupWidget/widget";
const isVisible = {
"!type": "bool",
@ -173,7 +174,7 @@ export const entityDefinitions = {
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
},
isDisabled: "bool",
options: "[dropdownOption]",
options: "[$__dropdownOption__$]",
},
SELECT_WIDGET: {
"!doc":
@ -195,7 +196,7 @@ export const entityDefinitions = {
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
},
isDisabled: "bool",
options: "[dropdownOption]",
options: "[$__dropdownOption__$]",
},
MULTI_SELECT_WIDGET: {
"!doc":
@ -217,7 +218,7 @@ export const entityDefinitions = {
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
},
isDisabled: "bool",
options: "[dropdownOption]",
options: "[$__dropdownOption__$]",
},
MULTI_SELECT_WIDGET_V2: {
"!doc":
@ -239,7 +240,7 @@ export const entityDefinitions = {
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
},
isDisabled: "bool",
options: "[dropdownOption]",
options: "[$__dropdownOption__$]",
},
IMAGE_WIDGET: {
"!doc":
@ -264,6 +265,14 @@ export const entityDefinitions = {
isDisabled: "bool",
recaptchaToken: "string",
},
BUTTON_GROUP_WIDGET: (widget: ButtonGroupWidgetProps) => {
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",
},
};

View File

@ -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 },
},
];

View File

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

View File

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

View File

@ -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,
);
});
});

View File

@ -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<string, DataTreeDefEntityInformation> } => {
const def: any = {
const def: Def = {
"!name": "DATA_TREE",
};
const entityMap: Map<string, DataTreeDefEntityInformation> = 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<string, MetaArgs> = entity.meta;
const jsOptions: Record<string, unknown> = {};
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<string, string | Record<string, unknown>> {
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<string, string | Record<string, unknown>> = {};
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<string>();
// 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()`;
};

View File

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