Skip to content

Commit

Permalink
Implement support for code completion inside embedded expression of (…
Browse files Browse the repository at this point in the history
…only) string templates (not text block templates) (eclipse-jdt#1712)

* Implement support for code completion inside embedded expression of
(only) string templates (not text block templates)

* Fixes eclipse-jdt#1711
  • Loading branch information
srikanth-sankaran authored and jarthana committed Dec 13, 2023
1 parent 06e17ba commit 3567d6c
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10859,7 +10859,7 @@ public void test0164() {
" public X() {\n" +
" }\n" +
"}\n";
String expectedReplacedSource = "\"\\u005AZZZZ";
String expectedReplacedSource = "\"\\u005AZZZZ\\u000D\\u0022";
String testName = "<complete inside a string literal>";

int cursorLocation = str.indexOf(completeBehind) + completeBehind.length() - 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,39 @@ public static void main(String[] args) {
expectedReplacedSource,
testName);
}
// test selection after template
public void test005() throws JavaModelException {
String string =
"public class X {\n"
+ " public static void main(String[] args) {\n"
+ " String[] fruit = { \"apples\", \"oranges\", \"peaches\" };\n"
+ " String s = STR.\"\\{fruit[0]}, \\{STR.\"\\{/*here*/fruit[1]}, \\{fruit[2]}\"}\\u002e\";\n"
+ " System.out.println(s);\n"
+ " System.out.println(s.hashCode());\n"
+ " }\n"
+ "}";

String selection = "hashCode";
String expectedSelection = "<SelectOnMessageSend:s.hashCode()>";

String selectionIdentifier = "hashCode";
String expectedUnitDisplayString =
"public class X {\n" +
" public X() {\n" +
" }\n" +
" public static void main(String[] args) {\n" +
" String[] fruit;\n" +
" String s;\n" +
" System.out.println(<SelectOnMessageSend:s.hashCode()>);\n" +
" }\n" +
"}\n";
String expectedReplacedSource = "s.hashCode()";
String testName = "X.java";

int selectionStart = string.lastIndexOf(selection);
int selectionEnd = string.lastIndexOf(selection) + selection.length() - 1;

checkMethodParse(string.toCharArray(), selectionStart, selectionEnd, expectedSelection, expectedUnitDisplayString,
selectionIdentifier, expectedReplacedSource, testName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*******************************************************************************
* Copyright (c) 2023 Advantest Europe GmbH and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
*
https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Srikanth Sankaran - initial implementation
*******************************************************************************/
package org.eclipse.jdt.core.tests.model;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import junit.framework.Test;

public class EmbeddedExpressionCompletionTests extends AbstractJavaModelCompletionTests {

static {
// TESTS_NAMES = new String[]{"test034"};
}

public EmbeddedExpressionCompletionTests(String name) {
super(name);
}

public void setUpSuite() throws Exception {
if (COMPLETION_PROJECT == null) {
COMPLETION_PROJECT = setUpJavaProject("Completion", "21");
} else {
setUpProjectCompliance(COMPLETION_PROJECT, "21");
}
super.setUpSuite();
COMPLETION_PROJECT.setOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED);
}

public static Test suite() {
return buildModelTestSuite(EmbeddedExpressionCompletionTests.class);
}

public void test001() throws JavaModelException {
this.workingCopies = new ICompilationUnit[1];
this.workingCopies[0] = getWorkingCopy(
"/Completion/src/X.java",
"public class X {\n" +
" static String name = \"Jay\";\n" +
" public static void main(String[] args) {\n" +
" String s = STR.\"Hello \\{/*here*/na}\";\n" +
" System.out.println(s);\n" +
" }\n" +
"}\n"
);
CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true);
requestor.allowAllRequiredProposals();
String str = this.workingCopies[0].getSource();
String completeBehind = "/*here*/na";
int cursorLocation = str.indexOf(completeBehind) + completeBehind.length();
this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner);
assertResults("name[FIELD_REF]{name, LX;, Ljava.lang.String;, name, null, 52}",
requestor.getResults());

}
public void test002() throws JavaModelException {
this.workingCopies = new ICompilationUnit[1];
this.workingCopies[0] = getWorkingCopy(
"/Completion/src/X.java",
"public class X {\n"
+ " public static void main(String[] args) {\n"
+ " String[] fruit = { \"apples\", \"oranges\", \"peaches\" };\n"
+ " String s = STR.\"\\{fruit[0]}, \\{STR.\"\\{/*here*/fruit[1].has}, \\{fruit[2]}\"}\\u002e\";\n"
+ " System.out.println(s);\n"
+ " }\n"
+ "}"
);
CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true);
requestor.allowAllRequiredProposals();
String str = this.workingCopies[0].getSource();
String completeBehind = "has";
int cursorLocation = str.indexOf(completeBehind) + completeBehind.length();
this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner);
assertResults("hashCode[METHOD_REF]{hashCode(), Ljava.lang.Object;, ()I, hashCode, null, 60}",
requestor.getResults());

}
// test completion after template
public void test003() throws JavaModelException {
this.workingCopies = new ICompilationUnit[1];
this.workingCopies[0] = getWorkingCopy(
"/Completion/src/X.java",
"public class X {\n"
+ " public static void main(String[] args) {\n"
+ " String[] fruit = { \"apples\", \"oranges\", \"peaches\" };\n"
+ " String s = STR.\"\\{fruit[0]}, \\{STR.\"\\{fruit[1]}, \\{fruit[2]}\"}\\u002e\";\n"
+ " System.out.println(s);\n"
+ " System.out.println(s.has);\n"
+ " }\n"
+ "}"
);
CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true);
requestor.allowAllRequiredProposals();
String str = this.workingCopies[0].getSource();
String completeBehind = "has";
int cursorLocation = str.indexOf(completeBehind) + completeBehind.length();
this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner);
assertResults("hashCode[METHOD_REF]{hashCode(), Ljava.lang.Object;, ()I, hashCode, null, 90}",
requestor.getResults());

}
// test completion before template
public void test004() throws JavaModelException {
this.workingCopies = new ICompilationUnit[1];
this.workingCopies[0] = getWorkingCopy(
"/Completion/src/X.java",
"public class X {\n"
+ " public static void main(String[] args) {\n"
+ " System.out.println(args[0].has);\n"
+ " String[] fruit = { \"apples\", \"oranges\", \"peaches\" };\n"
+ " String s = STR.\"\\{fruit[0]}, \\{STR.\"\\{/*here*/fruit[1].has}, \\{fruit[2]}\"}\\u002e\";\n"
+ " System.out.println(s);\n"
+ " }\n"
+ "}"
);
CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true);
requestor.allowAllRequiredProposals();
String str = this.workingCopies[0].getSource();
String completeBehind = "has";
int cursorLocation = str.indexOf(completeBehind) + completeBehind.length();
this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner);
assertResults("hashCode[METHOD_REF]{hashCode(), Ljava.lang.Object;, ()I, hashCode, null, 90}",
requestor.getResults());

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class RunCompletionModelTests extends junit.framework.TestCase {
COMPLETION_SUITES.add(CompletionTests16_1.class);
COMPLETION_SUITES.add(CompletionTests16_2.class);
COMPLETION_SUITES.add(CompletionTests17.class);
COMPLETION_SUITES.add(EmbeddedExpressionCompletionTests.class);
COMPLETION_SUITES.add(CompletionTestsForRecordPattern.class);
COMPLETION_SUITES.add(CompletionContextTests.class);
COMPLETION_SUITES.add(CompletionContextTests_1_5.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5900,6 +5900,26 @@ public MethodDeclaration parseSomeStatements(int start, int end, int fakeBlocksC
}
return fakeMethod;
}
@Override
protected Expression parseEmbeddedExpression(Parser parser, char[] source, int offset, int length,
CompilationUnitDeclaration unit, boolean recordLineSeparators) {
Expression e = super.parseEmbeddedExpression(parser, source, offset, length, unit, recordLineSeparators);
if (((AssistParser) parser).assistNode != null) {
this.assistNode = ((AssistParser) parser).assistNode;
((CompletionScanner) this.scanner).completionIdentifier = ((CompletionScanner)parser.scanner).completionIdentifier;
((CompletionScanner) this.scanner).completedIdentifierStart = ((CompletionScanner)parser.scanner).completedIdentifierStart;
((CompletionScanner) this.scanner).completedIdentifierEnd = ((CompletionScanner)parser.scanner).completedIdentifierEnd;
}
return e;
}
@Override
protected CompletionParser getEmbeddedExpressionParser() {
CompletionParser cp = new CompletionParser(this.problemReporter, this.storeSourceEnds, this.monitor);
cp.cursorLocation = this.cursorLocation;
CompletionScanner cs = (CompletionScanner)cp.scanner;
cs.cursorLocation = this.cursorLocation;
return cp;
}
protected void popUntilCompletedAnnotationIfNecessary() {
if(this.elementPtr < 0) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,133 +442,25 @@ protected int getNextToken0() throws InvalidInputException {
}
throw invalidCharacter();
case '"' :
boolean isTextBlock = scanForTextBlockBeginning();
if (isTextBlock) {
return scanForTextBlock();
}
try {
// consume next character
this.unicodeAsBackSlash = false;
boolean isUnicode = false;
if (((this.currentCharacter = this.source[this.currentPosition++]) == '\\')
&& (this.source[this.currentPosition] == 'u')) {
getNextUnicodeChar();
isUnicode = true;
} else {
if (this.withoutUnicodePtr != 0) {
unicodeStore();
}
}

while (this.currentCharacter != '"') {
/**** \r and \n are not valid in string literals ****/
if ((this.currentCharacter == '\n') || (this.currentCharacter == '\r')) {
if (isUnicode) {
int start = this.currentPosition - 5;
while(this.source[start] != '\\') {
start--;
}
if(this.startPosition <= this.cursorLocation
&& this.cursorLocation <= this.currentPosition-1) {
this.currentPosition = start;
// complete inside a string literal
return TokenNameStringLiteral;
}
start = this.currentPosition;
for (int lookAhead = 0; lookAhead < 50; lookAhead++) {
if (this.currentPosition >= this.eofPosition) {
this.currentPosition = start;
break;
}
if (((this.currentCharacter = this.source[this.currentPosition++]) == '\\') && (this.source[this.currentPosition] == 'u')) {
isUnicode = true;
getNextUnicodeChar();
} else {
isUnicode = false;
}
if (!isUnicode && this.currentCharacter == '\n') {
this.currentPosition--; // set current position on new line character
break;
}
if (this.currentCharacter == '\"') {
throw invalidCharInString();
}
}
} else {
this.currentPosition--; // set current position on new line character
if(this.startPosition <= this.cursorLocation
&& this.cursorLocation <= this.currentPosition-1) {
// complete inside a string literal
return TokenNameStringLiteral;
}
}
throw invalidCharInString();
}
if (this.currentCharacter == '\\') {
if (this.unicodeAsBackSlash) {
this.withoutUnicodePtr--;
// consume next character
this.unicodeAsBackSlash = false;
if (((this.currentCharacter = this.source[this.currentPosition++]) == '\\') && (this.source[this.currentPosition] == 'u')) {
getNextUnicodeChar();
isUnicode = true;
this.withoutUnicodePtr--;
} else {
isUnicode = false;
}
} else {
if (this.withoutUnicodePtr == 0) {
unicodeInitializeBuffer(this.currentPosition - this.startPosition);
}
this.withoutUnicodePtr --;
this.currentCharacter = this.source[this.currentPosition++];
}
// we need to compute the escape character in a separate buffer
scanEscapeCharacter();
if (this.withoutUnicodePtr != 0) {
unicodeStore();
}
}
// consume next character
this.unicodeAsBackSlash = false;
if (((this.currentCharacter = this.source[this.currentPosition++]) == '\\')
&& (this.source[this.currentPosition] == 'u')) {
getNextUnicodeChar();
isUnicode = true;
} else {
isUnicode = false;
if (this.withoutUnicodePtr != 0) {
unicodeStore();
}
int ret = scanForStringLiteral();
return ret;
} catch(InvalidInputException e){
if (Scanner.INVALID_CHAR_IN_STRING.equals(e.getMessage())) {
if (this.startPosition <= this.cursorLocation
&& this.cursorLocation <= this.currentPosition-1) {
// complete inside a string literal
return TokenNameStringLiteral;
}

}
} catch (IndexOutOfBoundsException e) {
this.currentPosition--;
if(this.startPosition <= this.cursorLocation
&& this.cursorLocation < this.currentPosition) {
// complete inside a string literal
return TokenNameStringLiteral;
}
throw unterminatedString();
} catch (InvalidInputException e) {
if (e.getMessage().equals(INVALID_ESCAPE)) {
// relocate if finding another quote fairly close: thus unicode '/u000D' will be fully consumed
for (int lookAhead = 0; lookAhead < 50; lookAhead++) {
if (this.currentPosition + lookAhead == this.eofPosition)
break;
if (this.source[this.currentPosition + lookAhead] == '\n')
break;
if (this.source[this.currentPosition + lookAhead] == '\"') {
this.currentPosition += lookAhead + 1;
break;
}
} else if (Scanner.UNTERMINATED_STRING.equals(e.getMessage())) {
if (this.startPosition <= this.cursorLocation
&& this.cursorLocation <= this.currentPosition-1) {
// complete inside a string literal
return TokenNameStringLiteral;
}

}
throw e; // rethrow
throw e;
}
return TokenNameStringLiteral;
case '/' :
{
int test;
Expand Down

0 comments on commit 3567d6c

Please sign in to comment.