From 2ec11a4dee41936342a3249a48d99f6e0b9e3db5 Mon Sep 17 00:00:00 2001 From: Rishabh Rathod Date: Fri, 19 Aug 2022 19:09:07 +0530 Subject: [PATCH] fix: Autocomplete object value binding (#15999) ## Description Fixes #15950 Fixes #16141 ## Type of change - Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? **Test plan** - [ ] https://github.com/appsmithorg/TestSmith/issues/1982 - [ ] https://github.com/appsmithorg/TestSmith/issues/2049 ## Checklist: - [x] My code follows the style guidelines of this project - [ ] 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 - [ ] 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 --- .../src/utils/autocomplete/TernServer.test.ts | 53 +++++-- .../src/utils/autocomplete/TernServer.ts | 135 +++++++++++++++--- 2 files changed, 153 insertions(+), 35 deletions(-) diff --git a/app/client/src/utils/autocomplete/TernServer.test.ts b/app/client/src/utils/autocomplete/TernServer.test.ts index 08a20e62cc..0adbb04b0b 100644 --- a/app/client/src/utils/autocomplete/TernServer.test.ts +++ b/app/client/src/utils/autocomplete/TernServer.test.ts @@ -18,6 +18,7 @@ describe("Tern server", () => { doc: ({ getCursor: () => ({ ch: 0, line: 0 }), getLine: () => "{{Api.}}", + getValue: () => "{{Api.}}", } as unknown) as CodeMirror.Doc, changed: null, }, @@ -29,6 +30,7 @@ describe("Tern server", () => { doc: ({ getCursor: () => ({ ch: 0, line: 0 }), getLine: () => "a{{Api.}}", + getValue: () => "a{{Api.}}", } as unknown) as CodeMirror.Doc, changed: null, }, @@ -38,17 +40,30 @@ describe("Tern server", () => { input: { name: "test", doc: ({ - getCursor: () => ({ ch: 2, line: 0 }), - getLine: () => "a{{Api.}}", + getCursor: () => ({ ch: 10, line: 0 }), + getLine: () => "a{{Api.}}bc", + getValue: () => "a{{Api.}}bc", } as unknown) as CodeMirror.Doc, changed: null, }, - expectedOutput: "{{Api.}}", + expectedOutput: "a{{Api.}}bc", + }, + { + input: { + name: "test", + doc: ({ + getCursor: () => ({ ch: 4, line: 0 }), + getLine: () => "a{{Api.}}", + getValue: () => "a{{Api.}}", + } as unknown) as CodeMirror.Doc, + changed: null, + }, + expectedOutput: "Api.", }, ]; testCases.forEach((testCase) => { - const value = TernServer.getFocusedDynamicValue(testCase.input); + const { value } = TernServer.getFocusedDocValueAndPos(testCase.input); expect(value).toBe(testCase.expectedOutput); }); }); @@ -62,7 +77,7 @@ describe("Tern server", () => { getCursor: () => ({ ch: 0, line: 0 }), getLine: () => "{{Api.}}", somethingSelected: () => false, - getValue: () => "", + getValue: () => "{{Api.}}", } as unknown) as CodeMirror.Doc, changed: null, }, @@ -75,7 +90,7 @@ describe("Tern server", () => { getCursor: () => ({ ch: 0, line: 0 }), getLine: () => "{{Api.}}", somethingSelected: () => false, - getValue: () => "", + getValue: () => "{{Api.}}", } as unknown) as CodeMirror.Doc, changed: null, }, @@ -85,20 +100,32 @@ describe("Tern server", () => { input: { name: "test", doc: ({ - getCursor: () => ({ ch: 3, line: 0 }), + getCursor: () => ({ ch: 8, line: 0 }), getLine: () => "g {{Api.}}", somethingSelected: () => false, - getValue: () => "", + getValue: () => "g {{Api.}}", } as unknown) as CodeMirror.Doc, changed: null, }, - expectedOutput: { ch: 3, line: 0 }, + expectedOutput: { ch: 4, line: 0 }, + }, + { + input: { + name: "test", + doc: ({ + getCursor: () => ({ ch: 7, line: 1 }), + getLine: () => "c{{Api.}}", + somethingSelected: () => false, + getValue: () => "ab\nc{{Api.}}", + } as unknown) as CodeMirror.Doc, + changed: null, + }, + expectedOutput: { ch: 4, line: 0 }, }, ]; testCases.forEach((testCase) => { const request = TernServer.buildRequest(testCase.input, {}); - expect(request.query.end).toEqual(testCase.expectedOutput); }); }); @@ -115,6 +142,7 @@ describe("Tern server", () => { getCursor: () => ({ ch: 2, line: 0 }), getLine: () => "{{}}", somethingSelected: () => false, + getValue: () => "{{}}", } as unknown) as CodeMirror.Doc, }, requestCallbackData: { @@ -134,12 +162,13 @@ describe("Tern server", () => { getCursor: () => ({ ch: 3, line: 0 }), getLine: () => " {{}}", somethingSelected: () => false, + getValue: () => " {{}}", } as unknown) as CodeMirror.Doc, }, requestCallbackData: { completions: [{ name: "Api1" }], - start: { ch: 2, line: 0 }, - end: { ch: 6, line: 0 }, + start: { ch: 0, line: 0 }, + end: { ch: 4, line: 0 }, }, }, expectedOutput: { ch: 3, line: 0 }, diff --git a/app/client/src/utils/autocomplete/TernServer.ts b/app/client/src/utils/autocomplete/TernServer.ts index 810be7922e..2626bd3b54 100644 --- a/app/client/src/utils/autocomplete/TernServer.ts +++ b/app/client/src/utils/autocomplete/TernServer.ts @@ -207,20 +207,20 @@ class TernServer { } const doc = this.findDoc(cm.getDoc()); const cursor = cm.getCursor(); - const lineValue = this.lineValue(doc); - const focusedValue = this.getFocusedDynamicValue(doc); - const index = lineValue.indexOf(focusedValue); + const { extraChars } = this.getFocusedDocValueAndPos(doc); + let completions: Completion[] = []; let after = ""; const { end, start } = data; + const from = { ...start, - ch: start.ch + index, + ch: start.ch + extraChars, line: cursor.line, }; const to = { ...end, - ch: end.ch + index, + ch: end.ch + extraChars, line: cursor.line, }; if ( @@ -392,7 +392,7 @@ class TernServer { addDoc(name: string, doc: CodeMirror.Doc) { const data = { doc: doc, name: name, changed: null }; - this.server.addFile(name, this.getFocusedDynamicValue(data)); + this.server.addFile(name, this.getFocusedDocValueAndPos(data).value); CodeMirror.on(doc, "change", this.trackChange.bind(this)); return (this.docs[name] = data); } @@ -412,7 +412,13 @@ class TernServer { query.depth = 0; query.sort = true; if (query.end == null) { - query.end = pos || doc.doc.getCursor("end"); + const positions = pos || doc.doc.getCursor("end"); + const { end } = this.getFocusedDocValueAndPos(doc); + query.end = { + ...positions, + ...end, + }; + if (doc.doc.somethingSelected()) query.start = doc.doc.getCursor("start"); } const startPos = query.start || query.end; @@ -435,7 +441,7 @@ class TernServer { files.push({ type: "full", name: doc.name, - text: this.docValue(doc), + text: this.getFocusedDocValueAndPos(doc).value, }); query.file = doc.name; doc.changed = null; @@ -448,7 +454,7 @@ class TernServer { files.push({ type: "full", name: doc.name, - text: this.docValue(doc), + text: this.getFocusedDocValueAndPos(doc).value, }); } for (const name in this.docs) { @@ -457,7 +463,7 @@ class TernServer { files.push({ type: "full", name: cur.name, - text: this.docValue(cur), + text: this.getFocusedDocValueAndPos(doc).value, }); cur.changed = null; } @@ -508,7 +514,7 @@ class TernServer { { type: "full", name: doc.name, - text: this.getFocusedDynamicValue(doc), + text: this.docValue(doc), }, ], }, @@ -529,23 +535,106 @@ class TernServer { return doc.doc.getValue(); } - getFocusedDynamicValue(doc: TernDoc) { - const cursor = doc.doc.getCursor(); - const value = this.lineValue(doc); - const stringSegments = getDynamicStringSegments(value); - const dynamicStrings = stringSegments.filter((segment) => { - if (isDynamicValue(segment)) { - const index = value.indexOf(segment); + getFocusedDocValueAndPos( + doc: TernDoc, + ): { value: string; end: { line: number; ch: number }; extraChars: number } { + const cursor = doc.doc.getCursor("end"); + const value = this.docValue(doc); + const lineValue = this.lineValue(doc); + let extraChars = 0; - if (cursor.ch >= index && cursor.ch <= index + segment.length) { - return true; + const stringSegments = getDynamicStringSegments(value); + if (stringSegments.length === 1) { + return { + value, + end: { + line: cursor.line, + ch: cursor.ch, + }, + extraChars, + }; + } + + let dynamicString = value; + + let newCursorLine = cursor.line; + let newCursorPosition = cursor.ch; + + let currentLine = 0; + + for (let index = 0; index < stringSegments.length; index++) { + // segment is divided according to binding {{}} + + const segment = stringSegments[index]; + let currentSegment = segment; + if (segment.startsWith("{{")) { + currentSegment = segment.replace("{{", ""); + if (currentSegment.endsWith("}}")) { + currentSegment = currentSegment.slice(0, currentSegment.length - 2); } } - return false; - }); + // subSegment is segment further divided by EOD char (\n) + const subSegments = currentSegment.split("\n"); + const countEODCharInSegment = subSegments.length - 1; + const segmentEndLine = countEODCharInSegment + currentLine; - return dynamicStrings.length ? dynamicStrings[0] : value; + /** + * 3 case for cursor to point inside segment + * 1. cursor is before the {{ :- + * 2. cursor is inside segment :- + * - if cursor is after {{ on same line + * - if cursor is after {{ in different line + * - if cursor is before }} on same line + * 3. cursor is after the }} :- + * + */ + + const isCursorInBetweenSegmentStartAndEndLine = + cursor.line > currentLine && cursor.line < segmentEndLine; + + const isCursorAtSegmentStartLine = cursor.line === currentLine; + const isCursorAfterBindingOpenAtSegmentStart = + isCursorAtSegmentStartLine && cursor.ch > lineValue.indexOf("{{") + 1; + const isCursorAtSegmentEndLine = cursor.line === segmentEndLine; + const isCursorBeforeBindingCloseAtSegmentEnd = + isCursorAtSegmentEndLine && cursor.ch < lineValue.indexOf("}}") + 1; + + const isSegmentStartLineAndEndLineSame = currentLine === segmentEndLine; + const isCursorBetweenSingleLineSegmentBinding = + isSegmentStartLineAndEndLineSame && + isCursorBeforeBindingCloseAtSegmentEnd && + isCursorAfterBindingOpenAtSegmentStart; + + const isCursorPointingInsideSegment = + isCursorInBetweenSegmentStartAndEndLine || + (isSegmentStartLineAndEndLineSame && + isCursorBetweenSingleLineSegmentBinding); + (!isSegmentStartLineAndEndLineSame && + isCursorBeforeBindingCloseAtSegmentEnd) || + isCursorAfterBindingOpenAtSegmentStart; + + if (isDynamicValue(segment) && isCursorPointingInsideSegment) { + dynamicString = currentSegment; + newCursorLine = cursor.line - currentLine; + if (lineValue.includes("{{")) { + extraChars = lineValue.indexOf("{{") + 2; + } + newCursorPosition = cursor.ch - extraChars; + + break; + } + currentLine = segmentEndLine; + } + + return { + value: dynamicString, + end: { + line: newCursorLine, + ch: newCursorPosition, + }, + extraChars, + }; } getFragmentAround(