From 3567d6c7caed0c33c19cbbfbccf8ba3b9dde448f Mon Sep 17 00:00:00 2001 From: Srikanth Sankaran <131454720+srikanth-sankaran@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:59:08 +0530 Subject: [PATCH] Implement support for code completion inside embedded expression of (only) string templates (not text block templates) (#1712) * Implement support for code completion inside embedded expression of (only) string templates (not text block templates) * Fixes https://github.com/eclipse-jdt/eclipse.jdt.core/issues/1711 --- .../parser/CompletionParserTest2.java | 2 +- .../EmbeddedExpressionSelectionTest.java | 35 +++++ .../EmbeddedExpressionCompletionTests.java | 140 ++++++++++++++++++ .../tests/model/RunCompletionModelTests.java | 1 + .../codeassist/complete/CompletionParser.java | 20 +++ .../complete/CompletionScanner.java | 136 ++--------------- 6 files changed, 211 insertions(+), 123 deletions(-) create mode 100644 org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/EmbeddedExpressionCompletionTests.java diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/parser/CompletionParserTest2.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/parser/CompletionParserTest2.java index 7247ee24923..427bf3760df 100644 --- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/parser/CompletionParserTest2.java +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/parser/CompletionParserTest2.java @@ -10859,7 +10859,7 @@ public void test0164() { " public X() {\n" + " }\n" + "}\n"; - String expectedReplacedSource = "\"\\u005AZZZZ"; + String expectedReplacedSource = "\"\\u005AZZZZ\\u000D\\u0022"; String testName = ""; int cursorLocation = str.indexOf(completeBehind) + completeBehind.length() - 1; diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/parser/EmbeddedExpressionSelectionTest.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/parser/EmbeddedExpressionSelectionTest.java index 60d30e94d45..e8ae885fc34 100644 --- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/parser/EmbeddedExpressionSelectionTest.java +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/parser/EmbeddedExpressionSelectionTest.java @@ -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 = ""; + + 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();\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); + } } \ No newline at end of file diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/EmbeddedExpressionCompletionTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/EmbeddedExpressionCompletionTests.java new file mode 100644 index 00000000000..0d535d3701f --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/EmbeddedExpressionCompletionTests.java @@ -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()); + + } + +} diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/RunCompletionModelTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/RunCompletionModelTests.java index f7576703715..d6ac6d09500 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/RunCompletionModelTests.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/RunCompletionModelTests.java @@ -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); diff --git a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionParser.java b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionParser.java index 8fe7a2924ad..fa54ffb9b70 100644 --- a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionParser.java +++ b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionParser.java @@ -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; diff --git a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionScanner.java b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionScanner.java index 517c5f7f26a..5923f8cc226 100644 --- a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionScanner.java +++ b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionScanner.java @@ -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;