diff --git a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts index 2cf42d270c..7ad268112c 100644 --- a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts +++ b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts @@ -73,6 +73,8 @@ export type Hinter = { export type MarkHelper = ( editor: CodeMirror.Editor, entityNavigationData: EntityNavigationData, + from?: CodeMirror.Position, + to?: CodeMirror.Position, ) => void; export enum CodeEditorBorder { diff --git a/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/bindingMarker.ts b/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/bindingMarker.ts new file mode 100644 index 0000000000..ff7bb8d84c --- /dev/null +++ b/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/bindingMarker.ts @@ -0,0 +1,41 @@ +import type CodeMirror from "codemirror"; +import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants"; +import type { MarkHelper } from "components/editorComponents/CodeEditor/EditorConfig"; + +export const bindingMarker: MarkHelper = (editor: CodeMirror.Editor) => { + editor.eachLine((line: CodeMirror.LineHandle) => { + const lineNo = editor.getLineNumber(line) || 0; + let match; + while ((match = AUTOCOMPLETE_MATCH_REGEX.exec(line.text)) != null) { + const opening = { + start: match.index, + end: match.index + 2, + }; + const ending = { + start: AUTOCOMPLETE_MATCH_REGEX.lastIndex - 2, + end: AUTOCOMPLETE_MATCH_REGEX.lastIndex, + }; + editor.markText( + { ch: ending.start, line: lineNo }, + { ch: ending.end, line: lineNo }, + { + className: "binding-brackets", + }, + ); + editor.markText( + { ch: opening.start, line: lineNo }, + { ch: opening.end, line: lineNo }, + { + className: "binding-brackets", + }, + ); + editor.markText( + { ch: opening.start, line: lineNo }, + { ch: ending.end, line: lineNo }, + { + className: "binding-highlight", + }, + ); + } + }); +}; diff --git a/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/entityMarker.ts b/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/entityMarker.ts new file mode 100644 index 0000000000..95b89065c6 --- /dev/null +++ b/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/entityMarker.ts @@ -0,0 +1,148 @@ +import type { + EntityNavigationData, + NavigationData, +} from "selectors/navigationSelectors"; +import type { MarkHelper } from "../EditorConfig"; + +export const NAVIGATE_TO_ATTRIBUTE = "data-navigate-to"; +export const NAVIGATION_CLASSNAME = "navigable-entity-highlight"; + +const hasReference = (token: CodeMirror.Token) => { + const tokenString = token.string; + return token.type === "variable" || tokenString === "this"; +}; + +export const PEEKABLE_CLASSNAME = "peekable-entity-highlight"; +export const PEEKABLE_ATTRIBUTE = "peek-data"; +export const PEEKABLE_LINE = "peek-line"; +export const PEEKABLE_CH_START = "peek-ch-start"; +export const PEEKABLE_CH_END = "peek-ch-end"; +export const PEEK_STYLE_PERSIST_CLASS = "peek-style-persist"; + +export const entityMarker: MarkHelper = ( + editor: CodeMirror.Editor, + entityNavigationData, + from, + to, +) => { + let markers: CodeMirror.TextMarker[] = []; + if (from && to) { + markers = editor.findMarks( + { + line: from.line, + ch: 0, + }, + { + line: to.line, + // when a line is deleted? + ch: editor.getLine(to.line).length - 1, + }, + ); + clearMarkers(markers); + + editor.eachLine(from.line, to.line, (line: CodeMirror.LineHandle) => { + addMarksForLine(editor, line, entityNavigationData); + }); + } else { + markers = editor.getAllMarks(); + clearMarkers(markers); + + editor.eachLine((line: CodeMirror.LineHandle) => { + addMarksForLine(editor, line, entityNavigationData); + }); + } +}; + +const addMarksForLine = ( + editor: CodeMirror.Editor, + line: CodeMirror.LineHandle, + entityNavigationData: EntityNavigationData, +) => { + const lineNo = editor.getLineNumber(line) || 0; + const tokens = editor.getLineTokens(lineNo); + tokens.forEach((token) => { + const tokenString = token.string; + if (hasReference(token) && tokenString in entityNavigationData) { + const data = entityNavigationData[tokenString]; + if (data.navigable || data.peekable) { + editor.markText( + { ch: token.start, line: lineNo }, + { ch: token.end, line: lineNo }, + getMarkOptions(data, token, lineNo), + ); + } + addMarksForChildren( + entityNavigationData[tokenString], + lineNo, + token.end, + editor, + ); + } + }); +}; + +const addMarksForChildren = ( + navigationData: NavigationData, + lineNo: number, + tokenEnd: number, + editor: CodeMirror.Editor, +) => { + const childNodes = navigationData.children || {}; + if (Object.keys(childNodes).length) { + const token = editor.getTokenAt( + { + ch: tokenEnd + 2, + line: lineNo, + }, + true, + ); + if (token.string in childNodes) { + const childLink = childNodes[token.string]; + if (childLink.navigable || childLink.peekable) { + editor.markText( + { ch: token.start, line: lineNo }, + { ch: token.end, line: lineNo }, + getMarkOptions(childLink, token, lineNo), + ); + } + addMarksForChildren(childNodes[token.string], lineNo, token.end, editor); + } + } +}; + +const getMarkOptions = ( + data: NavigationData, + token: CodeMirror.Token, + lineNo: number, +): CodeMirror.TextMarkerOptions => { + return { + className: `${data.navigable ? NAVIGATION_CLASSNAME : ""} ${ + data.peekable ? PEEKABLE_CLASSNAME : "" + }`, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + attributes: { + ...(data.navigable && { + [NAVIGATE_TO_ATTRIBUTE]: `${data.name}`, + }), + ...(data.peekable && { + [PEEKABLE_ATTRIBUTE]: data.name, + [PEEKABLE_CH_START]: token.start, + [PEEKABLE_CH_END]: token.end, + [PEEKABLE_LINE]: lineNo, + }), + }, + atomic: false, + title: data.name, + }; +}; + +const clearMarkers = (markers: CodeMirror.TextMarker[]) => { + markers.forEach((marker) => { + if ( + marker.className?.includes(NAVIGATION_CLASSNAME) || + marker.className?.includes(PEEKABLE_CLASSNAME) + ) + marker.clear(); + }); +}; diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index bbaf7174c6..28ef127ae7 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -55,8 +55,8 @@ import { EditorWrapper, IconContainer, } from "components/editorComponents/CodeEditor/styledComponents"; +import { bindingMarker } from "components/editorComponents/CodeEditor/MarkHelpers/bindingMarker"; import { - bindingMarker, entityMarker, NAVIGATE_TO_ATTRIBUTE, PEEKABLE_ATTRIBUTE, @@ -64,7 +64,7 @@ import { PEEKABLE_CH_START, PEEKABLE_LINE, PEEK_STYLE_PERSIST_CLASS, -} from "components/editorComponents/CodeEditor/markHelpers"; +} from "components/editorComponents/CodeEditor/MarkHelpers/entityMarker"; import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers"; import BindingPrompt from "./BindingPrompt"; import { showBindingPrompt } from "./BindingPromptHelper"; @@ -509,11 +509,16 @@ class CodeEditor extends Component { this.setEditorInput(""); } - CodeEditor.updateMarkings( - this.editor, - this.props.marking, - this.props.entitiesForNavigation, - ); + if ( + this.props.entitiesForNavigation !== prevProps.entitiesForNavigation || + this.props.marking !== prevProps.marking + ) { + CodeEditor.updateMarkings( + this.editor, + this.props.marking, + this.props.entitiesForNavigation, + ); + } }); } @@ -962,11 +967,13 @@ class CodeEditor extends Component { } } - if (this.editor) { + if (this.editor && changeObj) { CodeEditor.updateMarkings( this.editor, this.props.marking, this.props.entitiesForNavigation, + changeObj.from, + changeObj.to, ); } }; @@ -1127,8 +1134,10 @@ class CodeEditor extends Component { editor: CodeMirror.Editor, marking: Array, entityNavigationData: EntityNavigationData, + from?: CodeMirror.Position, + to?: CodeMirror.Position, ) => { - marking.forEach((helper) => helper(editor, entityNavigationData)); + marking.forEach((helper) => helper(editor, entityNavigationData, from, to)); }; updatePropertyValue(value: string, cursor?: number) { @@ -1362,12 +1371,10 @@ const mapStateToProps = (state: AppState, props: EditorProps) => ({ state, getEditorIdentifier(props), ), - entitiesForNavigation: props.isJSObject - ? addThisReference( - getEntitiesForNavigation(state), - props.dataTreePath?.split(".")[0], - ) - : getEntitiesForNavigation(state), + entitiesForNavigation: getEntitiesForNavigation( + state, + props.dataTreePath?.split(".")[0], + ), }); const mapDispatchToProps = (dispatch: any) => ({ @@ -1383,16 +1390,3 @@ const mapDispatchToProps = (dispatch: any) => ({ export default Sentry.withProfiler( connect(mapStateToProps, mapDispatchToProps)(CodeEditor), ); - -const addThisReference = ( - navigationData: EntityNavigationData, - entityName?: string, -) => { - if (entityName && entityName in navigationData) { - return { - ...navigationData, - this: navigationData[entityName], - }; - } - return navigationData; -}; diff --git a/app/client/src/components/editorComponents/CodeEditor/markHelpers.ts b/app/client/src/components/editorComponents/CodeEditor/markHelpers.ts deleted file mode 100644 index 3fae99ab28..0000000000 --- a/app/client/src/components/editorComponents/CodeEditor/markHelpers.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type CodeMirror from "codemirror"; -import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants"; -import type { MarkHelper } from "components/editorComponents/CodeEditor/EditorConfig"; -import type { NavigationData } from "selectors/navigationSelectors"; - -export const bindingMarker: MarkHelper = (editor: CodeMirror.Editor) => { - editor.eachLine((line: CodeMirror.LineHandle) => { - const lineNo = editor.getLineNumber(line) || 0; - let match; - while ((match = AUTOCOMPLETE_MATCH_REGEX.exec(line.text)) != null) { - const opening = { - start: match.index, - end: match.index + 2, - }; - const ending = { - start: AUTOCOMPLETE_MATCH_REGEX.lastIndex - 2, - end: AUTOCOMPLETE_MATCH_REGEX.lastIndex, - }; - editor.markText( - { ch: ending.start, line: lineNo }, - { ch: ending.end, line: lineNo }, - { - className: "binding-brackets", - }, - ); - editor.markText( - { ch: opening.start, line: lineNo }, - { ch: opening.end, line: lineNo }, - { - className: "binding-brackets", - }, - ); - editor.markText( - { ch: opening.start, line: lineNo }, - { ch: ending.end, line: lineNo }, - { - className: "binding-highlight", - }, - ); - } - }); -}; - -export const NAVIGATE_TO_ATTRIBUTE = "data-navigate-to"; -export const NAVIGATION_CLASSNAME = "navigable-entity-highlight"; - -const hasReference = (token: CodeMirror.Token) => { - const tokenString = token.string; - return token.type === "variable" || tokenString === "this"; -}; - -export const PEEKABLE_CLASSNAME = "peekaboo"; -export const PEEKABLE_ATTRIBUTE = "peek-data"; -export const PEEKABLE_LINE = "peek-line"; -export const PEEKABLE_CH_START = "peek-ch-start"; -export const PEEKABLE_CH_END = "peek-ch-end"; -export const PEEK_STYLE_PERSIST_CLASS = "peek-style-persist"; - -export const entityMarker: MarkHelper = ( - editor: CodeMirror.Editor, - entityNavigationData, -) => { - editor - .getAllMarks() - .filter( - (marker) => - marker.className === NAVIGATION_CLASSNAME || - marker.className === PEEKABLE_CLASSNAME, - ) - .forEach((marker) => marker.clear()); - - editor.eachLine((line: CodeMirror.LineHandle) => { - const lineNo = editor.getLineNumber(line) || 0; - const tokens = editor.getLineTokens(lineNo); - tokens.forEach((token) => { - const tokenString = token.string; - if (hasReference(token) && tokenString in entityNavigationData) { - const data = entityNavigationData[tokenString]; - editor.markText( - { ch: token.start, line: lineNo }, - { ch: token.end, line: lineNo }, - { - className: NAVIGATION_CLASSNAME, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - attributes: { - [NAVIGATE_TO_ATTRIBUTE]: `${data.name}`, - }, - atomic: false, - title: data.name, - }, - ); - if (data.peekable) { - editor.markText( - { ch: token.start, line: lineNo }, - { ch: token.end, line: lineNo }, - { - className: PEEKABLE_CLASSNAME, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - attributes: { - [PEEKABLE_ATTRIBUTE]: data.name, - [PEEKABLE_CH_START]: token.start, - [PEEKABLE_CH_END]: token.end, - [PEEKABLE_LINE]: lineNo, - }, - atomic: false, - title: data.name, - }, - ); - } - addMarksForChildren( - entityNavigationData[tokenString], - lineNo, - token.end, - editor, - ); - } - }); - }); -}; - -const addMarksForChildren = ( - navigationData: NavigationData, - lineNo: number, - tokenEnd: number, - editor: CodeMirror.Editor, -) => { - const childNodes = navigationData.children || {}; - if (Object.keys(childNodes).length) { - const token = editor.getTokenAt( - { - ch: tokenEnd + 2, - line: lineNo, - }, - true, - ); - if (token.string in childNodes) { - const childLink = childNodes[token.string]; - if (childLink.navigable) { - editor.markText( - { ch: token.start, line: lineNo }, - { ch: token.end, line: lineNo }, - { - className: NAVIGATION_CLASSNAME, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - attributes: { - [NAVIGATE_TO_ATTRIBUTE]: `${childLink.name}`, - }, - atomic: false, - title: childLink.name, - }, - ); - } - if (childLink.peekable) { - editor.markText( - { ch: token.start, line: lineNo }, - { ch: token.end, line: lineNo }, - { - className: PEEKABLE_CLASSNAME, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - attributes: { - [PEEKABLE_ATTRIBUTE]: childLink.name, - [PEEKABLE_CH_START]: token.start, - [PEEKABLE_CH_END]: token.end, - [PEEKABLE_LINE]: lineNo, - }, - atomic: false, - title: childLink.name, - }, - ); - } - addMarksForChildren(childNodes[token.string], lineNo, token.end, editor); - } - } -}; diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts index 2826255413..d2bd5e1976 100644 --- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts +++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts @@ -11,7 +11,7 @@ import { NAVIGATION_CLASSNAME, PEEKABLE_CLASSNAME, PEEK_STYLE_PERSIST_CLASS, -} from "./markHelpers"; +} from "./MarkHelpers/entityMarker"; const getBorderStyle = ( props: { theme: Theme } & { diff --git a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx index 9c0d052854..37501e7d0c 100644 --- a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx +++ b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx @@ -22,10 +22,9 @@ import { TabBehaviour, EditorSize, } from "components/editorComponents/CodeEditor/EditorConfig"; -import { - bindingMarker, - entityMarker, -} from "components/editorComponents/CodeEditor/markHelpers"; +import { bindingMarker } from "components/editorComponents/CodeEditor/MarkHelpers/bindingMarker"; + +import { entityMarker } from "components/editorComponents/CodeEditor/MarkHelpers/entityMarker"; import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers"; import StoreAsDatasource from "components/editorComponents/StoreAsDatasource"; import { urlGroupsRegexExp } from "constants/AppsmithActionConstants/ActionConstants"; diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 9058e71727..2d1de3f4a2 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -382,6 +382,7 @@ export const getActionsForCurrentPage = createSelector( }, ); +// Note: getJSCollectionsForCurrentPage (returns a new object everytime) export const getJSCollectionsForCurrentPage = createSelector( getCurrentPageId, getJSCollections, diff --git a/app/client/src/selectors/navigationSelectors.ts b/app/client/src/selectors/navigationSelectors.ts index d44f82e150..6f874b6211 100644 --- a/app/client/src/selectors/navigationSelectors.ts +++ b/app/client/src/selectors/navigationSelectors.ts @@ -6,7 +6,7 @@ import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { createSelector } from "reselect"; import { getActionsForCurrentPage, - getJSCollectionsForCurrentPage, + getJSCollections, getPlugins, } from "selectors/entitiesSelector"; import { getWidgets } from "sagas/selectors"; @@ -19,6 +19,7 @@ import { createNavData } from "utils/NavigationSelector/common"; import { getWidgetChildrenNavData } from "utils/NavigationSelector/WidgetChildren"; import { getJsChildrenNavData } from "utils/NavigationSelector/JsChildren"; import { getAppsmithNavData } from "utils/NavigationSelector/AppsmithNavData"; +import { isJSObject } from "ce/workers/Evaluation/evaluationUtils"; export type NavigationData = { name: string; @@ -36,11 +37,22 @@ export type EntityNavigationData = Record; export const getEntitiesForNavigation = createSelector( getActionsForCurrentPage, getPlugins, - getJSCollectionsForCurrentPage, + getJSCollections, getWidgets, getCurrentPageId, getDataTree, - (actions, plugins, jsActions, widgets, pageId, dataTree: DataTree) => { + (_: any, entityName: string | undefined) => entityName, + ( + actions, + plugins, + jsActions, + widgets, + pageId, + dataTree: DataTree, + entityName: string | undefined, + ) => { + // data tree retriggers this + jsActions = jsActions.filter((a) => a.config.pageId === pageId); const navigationData: EntityNavigationData = {}; if (!dataTree) return navigationData; @@ -49,6 +61,7 @@ export const getEntitiesForNavigation = createSelector( (plugin) => plugin.id === action.config.pluginId, ); const config = getActionConfig(action.config.pluginType); + // dataTree used to get entityDefinitions and peekData const result = getActionChildrenNavData(action, dataTree); if (!config) return; navigationData[action.config.name] = createNavData({ @@ -68,6 +81,7 @@ export const getEntitiesForNavigation = createSelector( }); jsActions.forEach((jsAction) => { + // dataTree for null check and peekData const result = getJsChildrenNavData(jsAction, pageId, dataTree); navigationData[jsAction.config.name] = createNavData({ id: jsAction.config.id, @@ -81,6 +95,7 @@ export const getEntitiesForNavigation = createSelector( }); Object.values(widgets).forEach((widget) => { + // dataTree to get entityDefinitions, for url (can use getWidgetByName?) and peekData const result = getWidgetChildrenNavData(widget, dataTree, pageId); navigationData[widget.widgetName] = createNavData({ id: widget.widgetId, @@ -92,9 +107,20 @@ export const getEntitiesForNavigation = createSelector( children: result?.childNavData || {}, }); }); + // dataTree to get entity definitions and peekData navigationData["appsmith"] = getAppsmithNavData( dataTree.appsmith as AppsmithEntity, ); + if ( + entityName && + isJSObject(dataTree[entityName]) && + entityName in navigationData + ) { + return { + ...navigationData, + this: navigationData[entityName], + }; + } return navigationData; }, );