feat: code commenting #9369 (#18667)

## Description
Adds code commenting in JS objects code editor and JS fields. Users can
use `Cmd + /` on Mac and `Ctrl + /` on other systems to
comment/uncomment code now.

Fixes #9369



## Type of change
- New feature (non-breaking change which adds functionality)

## How Has This Been Tested?
- Manual
- Jest

### Test Plan
- [x] https://github.com/appsmithorg/TestSmith/issues/2120
- [x] https://github.com/appsmithorg/TestSmith/issues/2121
- [x] https://github.com/appsmithorg/TestSmith/issues/2122

### Issues raised during DP testing
- [ ]
https://github.com/appsmithorg/appsmith/pull/18667#issuecomment-1348354145


## Checklist:
### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


### QA activity:
- [ ] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [ ] Cypress test cases have been added and approved by either SDET or
manual QA
- [ ] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test

Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
This commit is contained in:
Ravi Kumar Prasad 2023-01-06 17:27:53 +05:30 committed by GitHub
parent 8e4f267b5d
commit fa930838aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 848 additions and 9 deletions

View File

@ -0,0 +1,40 @@
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
const {
AggregateHelper,
CommonLocators,
EntityExplorer,
PropertyPane,
} = ObjectsRegistry;
describe("Property Pane Suggestions", () => {
before(() => {
cy.fixture("buttondsl").then((val: any) => {
AggregateHelper.AddDsl(val);
});
});
it("1. Should show Property Pane Suggestions on / command", () => {
EntityExplorer.SelectEntityByName("Button1", "Widgets");
PropertyPane.TypeTextIntoField("Label", "/");
AggregateHelper.GetNAssertElementText(CommonLocators._hints, "Bind Data");
AggregateHelper.GetNAssertElementText(
CommonLocators._hints,
"New Binding",
"have.text",
1,
);
AggregateHelper.GetNClickByContains(CommonLocators._hints, "New Binding");
PropertyPane.ValidatePropertyFieldValue("Label", "{{}}");
});
it("2. Should show Property Pane Suggestions on typing {{}}", () => {
EntityExplorer.SelectEntityByName("Button1", "Widgets");
PropertyPane.TypeTextIntoField("Label", "{{");
AggregateHelper.GetNAssertElementText(CommonLocators._hints, "appsmith");
AggregateHelper.GetNClickByContains(CommonLocators._hints, "appsmith");
PropertyPane.ValidatePropertyFieldValue("Label", "{{appsmith}}");
});
});

View File

@ -0,0 +1,27 @@
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
const { AggregateHelper, EntityExplorer, PropertyPane } = ObjectsRegistry;
describe("Property Pane Code Commenting", () => {
before(() => {
cy.fixture("buttondsl").then((val: any) => {
AggregateHelper.AddDsl(val);
});
});
it("1. Should comment code in Property Pane", () => {
EntityExplorer.SelectEntityByName("Button1", "Widgets");
PropertyPane.TypeTextIntoField("Label", "{{appsmith}}");
PropertyPane.ToggleCommentInTextField("Label");
PropertyPane.ValidatePropertyFieldValue("Label", "{{// appsmith}}");
});
it("2. Should uncomment code in Property Pane", () => {
EntityExplorer.SelectEntityByName("Button1", "Widgets");
PropertyPane.TypeTextIntoField("Label", "{{// appsmith}}");
PropertyPane.ToggleCommentInTextField("Label");
PropertyPane.ValidatePropertyFieldValue("Label", "{{appsmith}}");
});
});

View File

@ -0,0 +1,49 @@
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
let jsEditor = ObjectsRegistry.JSEditor,
agHelper = ObjectsRegistry.AggregateHelper;
describe("JSEditor Comment - Visual tests", () => {
it("1. comments code on the editor", () => {
jsEditor.CreateJSObject(
`export default {
myFun1: () => {
function hi(a,b) {
console.log(a,b);
}
hi(1,2);
},
myFun2: async () => {
//use async-await or promises
}
}`,
{
paste: true,
completeReplace: true,
toRun: false,
shouldCreateNewJSObj: true,
prettify: false,
},
);
agHelper.GetNClick("[name='expand-more']", 1, true, 100);
agHelper.WaitUntilAllToastsDisappear();
cy.get("div.CodeMirror").matchImageSnapshot("jsObjBeforeCommenting1");
// Comment out lines 2,3,4
for (let i = 2; i < 5; i++) {
agHelper.GetNClick(jsEditor._lineinJsEditor(i));
agHelper.Sleep(100);
cy.get(jsEditor._lineinJsEditor(i)).type(
agHelper.isMac ? "{meta} /" : "{ctrl} /",
);
}
// Allow time to comment out lines
agHelper.Sleep(1000);
cy.get("div.CodeMirror").matchImageSnapshot("jsObjAfterCommenting1");
});
});

View File

@ -26,7 +26,7 @@ const refactorInput = {
},
};
describe("Validate JS Object Refactoring does not affect the comments & variables", () => {
describe.skip("Validate JS Object Refactoring does not affect the comments & variables", () => {
before(() => {
cy.fixture("Datatypes/RefactorDTdsl").then((val: any) => {
_.agHelper.AddDsl(val);

View File

@ -19,7 +19,7 @@ const DEFAULT_ENTERVALUE_OPTIONS = {
export class AggregateHelper {
private locator = ObjectsRegistry.CommonLocators;
private isMac = Cypress.platform === "darwin";
public isMac = Cypress.platform === "darwin";
private selectLine = `${
this.isMac ? "{cmd}{shift}{leftArrow}" : "{shift}{home}"
}`;

View File

@ -224,6 +224,17 @@ export class PropertyPane {
toVerifySave && this.agHelper.AssertAutoSave(); //Allowing time for saving entered value
}
public ValidatePropertyFieldValue(
propFieldName: string,
valueToValidate: string,
) {
cy.xpath(this.locator._existingFieldTextByName(propFieldName)).then(
($field: any) => {
this.agHelper.ValidateCodeEditorContent($field, valueToValidate);
},
);
}
public RemoveText(endp: string, toVerifySave = true) {
cy.get(
this.locator._propertyControl +
@ -264,6 +275,21 @@ export class PropertyPane {
this.agHelper.AssertAutoSave(); //Allowing time for saving entered value
}
public ToggleCommentInTextField(endp: string) {
cy.get(
this.locator._propertyControl +
endp.replace(/ +/g, "").toLowerCase() +
" " +
this.locator._codeMirrorTextArea,
)
.first()
.then((el: any) => {
cy.get(el).type(this.agHelper.isMac ? "{meta}/" : "{ctrl}/");
});
this.agHelper.AssertAutoSave(); //Allowing time for saving entered value
}
public EnterJSContext(
endp: string,
value: string,

View File

@ -18,6 +18,7 @@ import "codemirror/addon/mode/multiplex";
import "codemirror/addon/tern/tern.css";
import "codemirror/addon/lint/lint";
import "codemirror/addon/lint/lint.css";
import "codemirror/addon/comment/comment";
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup";
@ -114,6 +115,7 @@ import {
import { updateCustomDef } from "utils/autocomplete/customDefUtils";
import { shouldFocusOnPropertyControl } from "utils/editorContextUtils";
import { getEntityLintErrors } from "selectors/lintingSelectors";
import { getCodeCommentKeyMap, handleCodeComment } from "./utils/codeComment";
import {
EntityNavigationData,
getEntitiesForNavigation,
@ -201,6 +203,7 @@ export type EditorProps = EditorStyleProps &
// On focus and blur event handler
onEditorBlur?: () => void;
onEditorFocus?: () => void;
lineCommentString?: string;
};
interface Props extends ReduxStateProps, EditorProps, ReduxDispatchProps {}
@ -223,6 +226,7 @@ class CodeEditor extends Component<Props, State> {
static defaultProps = {
marking: [bindingMarker, entityMarker],
hinting: [bindingHint, commandsHelper],
lineCommentString: "//",
};
// this is the higlighted element for any highlighted text in the codemirror
highlightedUrlElement: HTMLElement | undefined;
@ -290,6 +294,11 @@ class CodeEditor extends Component<Props, State> {
const moveCursorLeftKey = getMoveCursorLeftKey();
options.extraKeys = {
[moveCursorLeftKey]: "goLineStartSmart",
[getCodeCommentKeyMap()]: handleCodeComment(
// We've provided the default props value for lineCommentString
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.props.lineCommentString!,
),
};
if (this.props.tabBehaviour === TabBehaviour.INPUT) {
@ -344,7 +353,7 @@ class CodeEditor extends Component<Props, State> {
//
editor.on("beforeChange", this.handleBeforeChange);
editor.on("change", this.startChange);
editor.on("keyup", this.handleAutocompleteKeyup);
editor.on("keydown", this.handleAutocompleteKeydown);
editor.on("focus", this.handleEditorFocus);
editor.on("cursorActivity", this.handleCursorMovement);
editor.on("blur", this.handleEditorBlur);
@ -535,7 +544,7 @@ class CodeEditor extends Component<Props, State> {
this.editor.off("beforeChange", this.handleBeforeChange);
this.editor.off("change", this.startChange);
this.editor.off("keyup", this.handleAutocompleteKeyup);
this.editor.off("keydown", this.handleAutocompleteKeydown);
this.editor.off("focus", this.handleEditorFocus);
this.editor.off("cursorActivity", this.handleCursorMovement);
this.editor.off("blur", this.handleEditorBlur);
@ -903,8 +912,16 @@ class CodeEditor extends Component<Props, State> {
this.setState({ hinterOpen });
};
handleAutocompleteKeyup = (cm: CodeMirror.Editor, event: KeyboardEvent) => {
handleAutocompleteKeydown = (cm: CodeMirror.Editor, event: KeyboardEvent) => {
const key = event.key;
// Since selection from AutoComplete list is also done using the Enter keydown event
// we need to return from here so that autocomplete selection works fine
if (key === "Enter") return;
// Check if the user is trying to comment out the line, in that case we should not show autocomplete
const isCtrlOrCmdPressed = event.metaKey || event.ctrlKey;
if (isModifierKey(key)) return;
const code = `${event.ctrlKey ? "Ctrl+" : ""}${event.code}`;
if (isCloseKey(code) || isCloseKey(key)) {
@ -916,20 +933,25 @@ class CodeEditor extends Component<Props, State> {
const line = cm.getLine(cursor.line);
let showAutocomplete = false;
/* Check if the character before cursor is completable to show autocomplete which backspacing */
if (key === "/") {
if (key === "/" && !isCtrlOrCmdPressed) {
showAutocomplete = true;
} else if (event.code === "Backspace") {
const prevChar = line[cursor.ch - 1];
showAutocomplete = !!prevChar && /[a-zA-Z_0-9.]/.test(prevChar);
} else if (key === "{") {
/* Autocomplete for "{" should show up only when a user attempts to write {{}} and not a code block. */
const prevChar = line[cursor.ch - 2];
/* Autocomplete for { should show up only when a user attempts to write {{}} and not a code block. */
const prevChar = line[cursor.ch - 1];
showAutocomplete = prevChar === "{";
} else if (key.length == 1) {
showAutocomplete = /[a-zA-Z_0-9.]/.test(key);
/* Autocomplete should be triggered only for characters that make up valid variable names */
}
showAutocomplete && this.handleAutocompleteVisibility(cm);
// Allow keydown event to enter the text to the editor before firing autocomplete
// otherwise it'll not work for the first character
setTimeout(() => {
showAutocomplete && this.handleAutocompleteVisibility(cm);
}, 10);
};
lintCode(editor: CodeMirror.Editor) {

View File

@ -0,0 +1,334 @@
import CodeMirror from "codemirror";
import { isMacOrIOS } from "utils/helpers";
import { EditorModes } from "../EditorConfig";
export const getCodeCommentKeyMap = () => {
return isMacOrIOS() ? "Cmd-/" : "Ctrl-/";
};
export function getLineCommentString(mode: EditorModes) {
switch (mode) {
case EditorModes.SQL:
case EditorModes.SQL_WITH_BINDING:
return "--";
default:
return "//";
}
}
// Most of the code below is copied from https://github.com/codemirror/codemirror5/blob/master/addon/comment/comment.js
// with minor modifications to support commenting in JS fields with {{ }} syntax
// CodeMirror's APIs don't allow such things, so copied functions and overrode them
/** Get end of line for line comment */
function getEndLineForLineComment(
from: CodeMirror.Position,
to: CodeMirror.Position,
cm: CodeMirror.Editor,
) {
return Math.min(
to.ch != 0 || to.line == from.line ? to.line + 1 : to.line,
cm.lastLine() + 1,
);
}
/** Get end of line for line comment */
function getEndLineForLineUncomment(
from: CodeMirror.Position,
to: CodeMirror.Position,
cm: CodeMirror.Editor,
) {
return Math.min(
to.ch != 0 || to.line == from.line ? to.line : to.line - 1,
cm.lastLine() + 1,
);
}
const JS_FIELD_BEGIN = "{{";
const JS_FIELD_END = "}}";
const nonWhitespace = /[^\s\u00a0]/;
const noOptions: CodeMirror.CommentOptions = {};
/**
* Gives index of the first non whitespace character in the line
**/
function firstNonWhitespace(str: string, mode: EditorModes) {
const found = str.search(
[EditorModes.JAVASCRIPT, EditorModes.TEXT_WITH_BINDING].includes(mode) &&
str.includes(JS_FIELD_BEGIN)
? JS_FIELD_BEGIN
: nonWhitespace,
);
return found === -1 ? 0 : found;
}
// Rough heuristic to try and detect lines that are part of multi-line string
function probablyInsideString(
cm: CodeMirror.Editor,
pos: CodeMirror.Position,
line: string,
) {
return (
/\bstring\b/.test(cm.getTokenTypeAt(CodeMirror.Pos(pos.line, 0))) &&
!/^[\'\"\`]/.test(line)
);
}
function performLineCommenting(
// this is a fake parameter to specify type for this
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#specifying-the-type-of-this-for-functions
this: CodeMirror.Editor,
from: CodeMirror.Position,
to: CodeMirror.Position,
options = noOptions,
) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: CodeMirror.Editor = this as any;
const mode = self.getMode();
const firstLine = self.getLine(from.line);
if (firstLine === null || probablyInsideString(self, from, firstLine)) return;
// When mode is TEXT, the name is null string, we skip commenting
const commentString =
mode.name === EditorModes.TEXT_WITH_BINDING &&
!(firstLine.includes(JS_FIELD_BEGIN) || firstLine.includes(JS_FIELD_END))
? ""
: options.lineComment || mode.lineComment;
if (!commentString) {
if (options.blockCommentStart || mode.blockCommentStart) {
options.fullLines = true;
self.blockComment(from, to, options);
}
return;
}
const end = getEndLineForLineComment(from, to, self);
const padding = options.padding || " ";
const blankLines = options.commentBlankLines || from.line === to.line;
self.operation(function() {
if (options.indent) {
for (let i = from.line; i < end; ++i) {
const line = self.getLine(i);
const baseString =
line.search(nonWhitespace) === -1
? line
: line.slice(
0,
firstNonWhitespace(
line,
// When there is JS bindings inside SQL, the mode is JAVASCRIPT instead of SQL
// we need to explicitly check if the SQL comment string is passed, make the mode SQL
commentString === getLineCommentString(EditorModes.SQL)
? EditorModes.SQL
: (mode.name as EditorModes),
),
);
const offset = (baseString || "").length;
if (!blankLines && !nonWhitespace.test(line)) continue;
// Handle JS field lines starting with {{
if (line.slice(offset).startsWith(JS_FIELD_BEGIN)) {
self.replaceRange(
baseString + JS_FIELD_BEGIN + commentString + padding,
CodeMirror.Pos(i, 0),
CodeMirror.Pos(i, offset + JS_FIELD_BEGIN.length),
);
continue;
}
self.replaceRange(
baseString + commentString + padding,
CodeMirror.Pos(i, 0),
CodeMirror.Pos(i, offset),
);
}
} else {
for (let i = from.line; i < end; ++i) {
const line = self.getLine(i);
if (blankLines || nonWhitespace.test(line)) {
// Handle JS field lines starting with {{
if (line.startsWith(JS_FIELD_BEGIN)) {
self.replaceRange(
commentString + padding,
CodeMirror.Pos(i, JS_FIELD_BEGIN.length),
);
continue;
}
self.replaceRange(commentString + padding, CodeMirror.Pos(i, 0));
}
}
}
});
}
function performLineUncommenting(
// this is a fake parameter to specify type for this
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#specifying-the-type-of-this-for-functions
this: CodeMirror.Editor,
from: CodeMirror.Position,
to: CodeMirror.Position,
options = noOptions,
) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const mode = self.getMode();
const end = getEndLineForLineUncomment(from, to, self);
const start = Math.min(from.line, end);
// Try finding line comments
const lineString = options.lineComment || mode.lineComment;
const lines: string[] = [];
const padding = options.padding || " ";
let didCommentCode;
lineComment: {
if (!lineString) break lineComment;
for (let i = start; i <= end; ++i) {
const line = self.getLine(i);
const found = line.indexOf(lineString);
if (found == -1 && nonWhitespace.test(line)) break lineComment;
if (
found > -1 &&
// Handle JS fields with {{}}
!line.trim().includes(JS_FIELD_BEGIN) &&
nonWhitespace.test(line.slice(0, found))
)
break lineComment;
lines.push(line);
}
self.operation(function() {
for (let i = start; i <= end; ++i) {
const line = lines[i - start];
const pos = line.indexOf(lineString);
let endPos = pos + lineString.length;
if (pos < 0) continue;
if (line.slice(endPos, endPos + padding.length) == padding)
endPos += padding.length;
didCommentCode = true;
self.replaceRange(
"",
CodeMirror.Pos(i, pos),
CodeMirror.Pos(i, endPos),
);
}
});
if (didCommentCode) return true;
}
// Try block comments
const startString = options.blockCommentStart || mode.blockCommentStart;
const endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) return false;
const blockCommentLead = options.blockCommentLead || mode.blockCommentLead;
const startLine = self.getLine(start);
const open = startLine.indexOf(startString);
if (open == -1) return false;
const endLine = end === start ? startLine : self.getLine(end);
const close = endLine.indexOf(
endString,
end === start ? open + startString.length : 0,
);
const insideStart = CodeMirror.Pos(start, open + 1),
insideEnd = CodeMirror.Pos(end, close + 1);
if (
close === -1 ||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||
!/comment/.test(self.getTokenTypeAt(insideEnd)) ||
self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1
)
return false;
// Avoid killing block comments completely outside the selection.
// Positions of the last startString before the start of the selection, and the first endString after it.
let lastStart = startLine.lastIndexOf(startString, from.ch);
let firstEnd =
lastStart === -1
? -1
: startLine
.slice(0, from.ch)
.indexOf(endString, lastStart + startString.length);
if (
lastStart !== -1 &&
firstEnd !== -1 &&
firstEnd + endString.length != from.ch
)
return false;
// Positions of the first endString after the end of the selection, and the last startString before it.
firstEnd = endLine.indexOf(endString, to.ch);
const almostLastStart = endLine
.slice(to.ch)
.lastIndexOf(startString, firstEnd - to.ch);
lastStart =
firstEnd === -1 || almostLastStart === -1 ? -1 : to.ch + almostLastStart;
if (firstEnd !== -1 && lastStart != -1 && lastStart !== to.ch) return false;
self.operation(function() {
self.replaceRange(
"",
CodeMirror.Pos(
end,
close -
(padding && endLine.slice(close - padding.length, close) == padding
? padding.length
: 0),
),
CodeMirror.Pos(end, close + endString.length),
);
let openEnd = open + startString.length;
if (
padding &&
startLine.slice(openEnd, openEnd + padding.length) == padding
)
openEnd += padding.length;
self.replaceRange(
"",
CodeMirror.Pos(start, open),
CodeMirror.Pos(start, openEnd),
);
if (blockCommentLead) {
for (let i = start + 1; i <= end; ++i) {
const line = self.getLine(i);
const found = line.indexOf(blockCommentLead);
if (found == -1 || nonWhitespace.test(line.slice(0, found))) continue;
let foundEnd = found + blockCommentLead.length;
if (
padding &&
line.slice(foundEnd, foundEnd + padding.length) == padding
)
foundEnd += padding.length;
self.replaceRange(
"",
CodeMirror.Pos(i, found),
CodeMirror.Pos(i, foundEnd),
);
}
}
});
return true;
}
/** This function handles commenting which includes functions copied from comment add on with modifications */
export const handleCodeComment = (lineCommentingString: string) => (
cm: CodeMirror.Editor,
) => {
cm.lineComment = performLineCommenting;
cm.uncomment = performLineUncommenting;
// This is the actual command that does the comment toggling
cm.toggleComment({
commentBlankLines: true,
// Always provide the line comment, otherwise it'll not work for JS fields when
// the mode is set to text/plain (when whole text wrapped in {{}} is selected)
lineComment: lineCommentingString,
indent: true,
});
};

View File

@ -0,0 +1,335 @@
import CodeMirror from "codemirror";
import "components/editorComponents/CodeEditor/modes";
import "codemirror/addon/comment/comment";
import { EditorModes } from "../EditorConfig";
import { handleCodeComment } from "./codeComment";
const JS_LINE_COMMENT = "//";
const SQL_LINE_COMMENT = "--";
describe("handleCodeComment", () => {
it("should handle code comment for single line", () => {
const editor = CodeMirror(document.body, { mode: EditorModes.JAVASCRIPT });
const code = `const a = 1;`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`// const a = 1;`);
});
it("should handle code comment for multiple lines", () => {
const editor = CodeMirror(document.body, { mode: EditorModes.JAVASCRIPT });
const code = `const a = 1;
const b = 2;`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`// const a = 1;
// const b = 2;`);
});
it("should handle code uncomment for multiple lines", () => {
const editor = CodeMirror(document.body, { mode: EditorModes.JAVASCRIPT });
const code = `// const a = 1;
// const b = 2;`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`const a = 1;
const b = 2;`);
});
it("should handle code comment for multiple lines in between", () => {
const editor = CodeMirror(document.body, { mode: EditorModes.JAVASCRIPT });
const code = `const a = 1;
const b = 2;
const c = 3;
const d = 4;`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection({ line: 1, ch: 0 }, { line: 3, ch: 0 });
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`const a = 1;
// const b = 2;
// const c = 3;
const d = 4;`);
});
it("should not code comment for JS fields with plain text only", () => {
const editor = CodeMirror(document.body, {
mode: EditorModes.TEXT_WITH_BINDING,
});
const code = `hello world`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`hello world`);
});
it("should handle code uncomment for JS fields with plain text", () => {
const editor = CodeMirror(document.body, {
mode: EditorModes.TEXT_WITH_BINDING,
});
const code = `// hello world`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`hello world`);
});
it("should handle code comment in JS fields with single line", () => {
const editor = CodeMirror(document.body, { mode: EditorModes.JAVASCRIPT });
const code = `{{ appsmith.store.id }}`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`{{// appsmith.store.id }}`);
});
it("should handle code comment in JS fields with text", () => {
const editor = CodeMirror(document.body, {
mode: EditorModes.JAVASCRIPT,
});
const code = `Hello {{ appsmith.store.id }}`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`Hello {{// appsmith.store.id }}`);
});
it("should handle code uncomment in JS fields with text", () => {
const editor = CodeMirror(document.body, {
mode: EditorModes.JAVASCRIPT,
});
const code = `Hello {{// appsmith.store.id }}`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`Hello {{ appsmith.store.id }}`);
});
it("should handle code comment in TEXT_WITH_BINDING fields with text", () => {
const editor = CodeMirror(document.body, {
mode: EditorModes.TEXT_WITH_BINDING,
});
const code = `"label": {{ appsmith.store.id }}`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`"label": {{// appsmith.store.id }}`);
});
it("should handle code comment in TEXT_WITH_BINDING fields with text in multiple lines", () => {
const editor = CodeMirror(document.body, {
mode: EditorModes.TEXT_WITH_BINDING,
});
const code = `"label": {{ 2
+ 2 }}`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 1, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`"label": {{ 2
// + 2 }}`);
});
it("should handle code comment in JS fields with multiple lines", () => {
const editor = CodeMirror(document.body, { mode: EditorModes.JAVASCRIPT });
const code = ` {{ (() => {
const a = "hello";
return "Text";
})()}}`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(` {{// (() => {
// const a = "hello";
// return "Text";
// })()}}`);
});
it("should handle code uncomment in JS fields with multiple lines", () => {
const editor = CodeMirror(document.body, { mode: EditorModes.JAVASCRIPT });
const code = ` {{// (() => {
// const a = "hello";
// return "Text";
// })()}}`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(JS_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(` {{(() => {
const a = "hello";
return "Text";
})()}}`);
});
it("should handle code comment for SQL queries", () => {
const editor = CodeMirror(document.body, {
mode: EditorModes.SQL,
});
const code = `Select * from users;`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(SQL_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(`-- Select * from users;`);
});
it("should handle code comment for SQL queries with JS bindings when cursor is placed outside JS bindings", () => {
const editor = CodeMirror(document.body, {
mode: EditorModes.SQL,
});
const code = `Select * from users where name={{Select.selectedOptionValue}};`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 0 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(SQL_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(
`-- Select * from users where name={{Select.selectedOptionValue}};`,
);
});
it("should handle code comment for SQL queries with JS bindings when cursor is placed inside JS bindings", () => {
const editor = CodeMirror(document.body, {
mode: EditorModes.SQL,
});
const code = `Select * from users where name={{Select.selectedOptionValue}};`;
editor.setValue(code);
// Select the code before commenting
editor.setSelection(
{ line: 0, ch: 18 },
{ line: editor.lastLine() + 1, ch: 0 },
);
handleCodeComment(SQL_LINE_COMMENT)(editor);
expect(editor.getValue()).toEqual(
`-- Select * from users where name={{Select.selectedOptionValue}};`,
);
});
});

View File

@ -23,6 +23,7 @@ class DynamicTextField extends React.Component<
showLightningMenu?: boolean;
height?: string;
disabled?: boolean;
lineCommentString?: string;
}
> {
render() {
@ -31,6 +32,7 @@ class DynamicTextField extends React.Component<
tabBehaviour: this.props.tabBehaviour || TabBehaviour.INPUT,
theme: this.props.theme || EditorTheme.LIGHT,
size: this.props.size || EditorSize.COMPACT,
lineCommentString: this.props.lineCommentString,
};
return <Field component={CodeEditor} {...this.props} {...editorProps} />;

View File

@ -15,6 +15,7 @@ import styled from "styled-components";
import { getPluginResponseTypes } from "selectors/entitiesSelector";
import { actionPathFromName } from "components/formControls/utils";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import { getLineCommentString } from "components/editorComponents/CodeEditor/utils/codeComment";
const Wrapper = styled.div`
width: 872px;
@ -65,6 +66,8 @@ class DynamicTextControl extends BaseControl<
? EditorModes.SQL_WITH_BINDING
: EditorModes.JSON_WITH_BINDING;
const lineCommentString = getLineCommentString(mode);
return (
<Wrapper className={`t--${configProperty}`}>
<DynamicTextField
@ -72,6 +75,7 @@ class DynamicTextControl extends BaseControl<
dataTreePath={dataTreePath}
disabled={this.props.disabled}
evaluationSubstitutionType={evaluationSubstitutionType}
lineCommentString={lineCommentString}
mode={mode}
name={this.props.configProperty}
placeholder={placeholderText}