Repository: camel-tooling/camel-idea-plugin
Issue: Realtime Validation : getIndex match the position in the string, but not the position of the caret.

Since Camel Catelog have no knowledge about the caret position it report index based on it's location in the String. 

But it could be nice if it was possible to call thecatalogService.validateSimpleExpression with an option to marker the position in the error message so we can replace the position with the editor caret position.

> Unknown function: romuteId at location \\<caret\\>33\\</caret\\> Response from Camel CDI on route ${romuteId} using thread: ${threadName}


Changed files:
camel-idea-plugin/src/main/java/org/apache/camel/idea/annotator/CamelSimpleAnnotator.java
camel-idea-plugin/src/test/java/org/apache/camel/idea/CamelLightCodeInsightFixtureTestCaseIT.java
camel-idea-plugin/src/main/java/org/apache/camel/idea/util/CamelIdeaUtils.java
camel-idea-plugin/src/test/java/org/apache/camel/idea/service/CamelCatalogServiceTestIT.java
camel-idea-plugin/src/test/java/org/apache/camel/idea/annotator/CamelSimpleAnnotatorTestIT.java

diff --git a/camel-idea-plugin/src/main/java/org/apache/camel/idea/annotator/CamelSimpleAnnotator.java b/camel-idea-plugin/src/main/java/org/apache/camel/idea/annotator/CamelSimpleAnnotator.java
index be0ee93..6188f4e 100644
--- a/camel-idea-plugin/src/main/java/org/apache/camel/idea/annotator/CamelSimpleAnnotator.java
+++ b/camel-idea-plugin/src/main/java/org/apache/camel/idea/annotator/CamelSimpleAnnotator.java
@@ -27,6 +27,7 @@ import org.apache.camel.idea.service.CamelCatalogService;
 import org.apache.camel.idea.service.CamelPreferenceService;
 import org.apache.camel.idea.service.CamelService;
 import org.apache.camel.idea.util.CamelIdeaUtils;
+import org.apache.camel.idea.util.IdeaUtils;
 import org.jetbrains.annotations.NotNull;
 
 import static org.apache.camel.idea.util.CamelIdeaUtils.isCameSimpleExpressionUsedAsPredicate;
@@ -49,7 +50,7 @@ public class CamelSimpleAnnotator extends AbstractCamelAnnotator {
      */
     void validateText(@NotNull PsiElement element, @NotNull AnnotationHolder holder, @NotNull String text) {
         boolean hasSimple = text.contains("${") || text.contains("$simple{");
-        if (hasSimple && CamelIdeaUtils.isCamelSimpleExpression(element)) {
+        if (hasSimple || CamelIdeaUtils.isCamelSimpleExpression(element)) {
             CamelCatalog catalogService = ServiceManager.getService(element.getProject(), CamelCatalogService.class).get();
             CamelService camelService = ServiceManager.getService(element.getProject(), CamelService.class);
 
@@ -58,10 +59,21 @@ public class CamelSimpleAnnotator extends AbstractCamelAnnotator {
                 ClassLoader loader = camelService.getCamelCoreClassloader();
                 if (loader != null) {
                     SimpleValidationResult result;
+
+                    int correctEndOffsetMinusOneOff = 2;
+                    int correctStartOffsetMinusOneOff = 1;
+
+                    if (IdeaUtils.isXmlLanguage(element)) {
+                        // the xml text range is one off compare to java text range
+                        correctEndOffsetMinusOneOff = 1;
+                        correctStartOffsetMinusOneOff = 0;
+                    }
+
                     boolean predicate = isCameSimpleExpressionUsedAsPredicate(element);
                     if (predicate) {
                         LOG.debug("Validate simple predicate: " + text);
                         result = catalogService.validateSimplePredicate(loader, text);
+                        correctEndOffsetMinusOneOff = 1; // the result for predicate index is minus one off compare to simple expression
                     } else {
                         LOG.debug("Validate simple expression: " + text);
                         result = catalogService.validateSimpleExpression(loader, text);
@@ -70,8 +82,17 @@ public class CamelSimpleAnnotator extends AbstractCamelAnnotator {
                         String error = result.getShortError();
                         TextRange range = element.getTextRange();
                         if (result.getIndex() > 0) {
-                            // use -1 to skip the last quote sign
-                            range = TextRange.create(range.getStartOffset() + result.getIndex(), range.getEndOffset() - 1);
+                            //we need to calculate the correct start and end position to be sure we highlight the correct word
+                            int startIdx = result.getIndex();
+                            //test if the simple expression is closed correctly
+                            int endIdx = text.indexOf("}", startIdx);
+                            if (endIdx == -1) {
+                                //the expression is not closed, test for first " " to see if can stop text range here
+                                endIdx = text.indexOf(" ", startIdx);
+                            }
+                            //calc the end index for highlighted word
+                            endIdx = endIdx == -1 ? (range.getEndOffset() - 1) : (range.getStartOffset() + endIdx) + correctEndOffsetMinusOneOff;
+                            range = TextRange.create(range.getStartOffset() + result.getIndex() + correctStartOffsetMinusOneOff, endIdx);
                         }
                         holder.createErrorAnnotation(range, error);
                     }
diff --git a/camel-idea-plugin/src/main/java/org/apache/camel/idea/util/CamelIdeaUtils.java b/camel-idea-plugin/src/main/java/org/apache/camel/idea/util/CamelIdeaUtils.java
index ee05f28..ab7f0a8 100644
--- a/camel-idea-plugin/src/main/java/org/apache/camel/idea/util/CamelIdeaUtils.java
+++ b/camel-idea-plugin/src/main/java/org/apache/camel/idea/util/CamelIdeaUtils.java
@@ -168,7 +168,7 @@ public final class CamelIdeaUtils {
                         element = element.getParent();
                     }
                     if (element instanceof PsiMethodCallExpression) {
-                        exp = (PsiMethodCallExpression) element;
+                        exp = PsiTreeUtil.getParentOfType(element.getParent(), PsiMethodCallExpression.class);
                     }
                 }
                 if (exp instanceof PsiMethodCallExpression) {
diff --git a/camel-idea-plugin/src/test/java/org/apache/camel/idea/CamelLightCodeInsightFixtureTestCaseIT.java b/camel-idea-plugin/src/test/java/org/apache/camel/idea/CamelLightCodeInsightFixtureTestCaseIT.java
index f786f91..7bf632b 100644
--- a/camel-idea-plugin/src/test/java/org/apache/camel/idea/CamelLightCodeInsightFixtureTestCaseIT.java
+++ b/camel-idea-plugin/src/test/java/org/apache/camel/idea/CamelLightCodeInsightFixtureTestCaseIT.java
@@ -19,10 +19,17 @@ package org.apache.camel.idea;
 import java.io.File;
 import java.io.IOException;
 import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.testFramework.LightProjectDescriptor;
+import com.intellij.testFramework.PsiTestUtil;
+import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
 import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
 import org.apache.camel.idea.service.CamelCatalogService;
 import org.apache.camel.idea.service.CamelService;
 import org.jboss.shrinkwrap.resolver.api.maven.Maven;
+import org.jetbrains.annotations.NotNull;
 
 
 /**
@@ -31,6 +38,17 @@ import org.jboss.shrinkwrap.resolver.api.maven.Maven;
  */
 public abstract class CamelLightCodeInsightFixtureTestCaseIT extends LightCodeInsightFixtureTestCase {
 
+    public static final String CAMEL_CORE_MAVEN_ARTIFACT = "org.apache.camel:camel-core:2.19.0-SNAPSHOT";
+    private static File[] mavenArtifacts;
+    private boolean ignoreCamelCoreLib;
+
+    static {
+        try {
+            mavenArtifacts = getMavenArtifacts(CAMEL_CORE_MAVEN_ARTIFACT);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -39,6 +57,19 @@ public abstract class CamelLightCodeInsightFixtureTestCaseIT extends LightCodeIn
         ServiceManager.getService(myModule.getProject(), CamelService.class).setCamelPresent(true);
     }
 
+    @Override
+    protected LightProjectDescriptor getProjectDescriptor() {
+
+        return new DefaultLightProjectDescriptor() {
+            @Override
+            public void configureModule(@NotNull Module module, @NotNull ModifiableRootModel model, @NotNull ContentEntry contentEntry) {
+                super.configureModule(module, model, contentEntry);
+                if (!isIgnoreCamelCoreLib()) {
+                    PsiTestUtil.addLibrary(module, model, "Maven: " + CAMEL_CORE_MAVEN_ARTIFACT, mavenArtifacts[0].getParent(), mavenArtifacts[0].getName());
+                }
+            }
+        };
+    }
     @Override
     protected String getTestDataPath() {
         return "src/test/resources/testData/";
@@ -53,8 +84,19 @@ public abstract class CamelLightCodeInsightFixtureTestCaseIT extends LightCodeIn
      * @return Array of artifact files
      * @throws IOException
      */
-    protected File[] getMavenArtifacts(String... mavneAritfiact) throws IOException {
-        File[] libs = Maven.resolver().loadPomFromFile("pom.xml").resolve(mavneAritfiact).withoutTransitivity().asFile();
+    protected static File[] getMavenArtifacts(String... mavneAritfiact) throws IOException {
+        File[] libs = Maven.resolver().loadPomFromFile("pom.xml")
+            .resolve(mavneAritfiact)
+            .withoutTransitivity().asFile();
+
         return libs;
     }
+
+    public boolean isIgnoreCamelCoreLib() {
+        return this.ignoreCamelCoreLib;
+    }
+
+    public void setIgnoreCamelCoreLib(boolean ignoreCamelCoreLib) {
+        this.ignoreCamelCoreLib = ignoreCamelCoreLib;
+    }
 }
diff --git a/camel-idea-plugin/src/test/java/org/apache/camel/idea/annotator/CamelSimpleAnnotatorTestIT.java b/camel-idea-plugin/src/test/java/org/apache/camel/idea/annotator/CamelSimpleAnnotatorTestIT.java
index 5c2783d..2b5ecd4 100644
--- a/camel-idea-plugin/src/test/java/org/apache/camel/idea/annotator/CamelSimpleAnnotatorTestIT.java
+++ b/camel-idea-plugin/src/test/java/org/apache/camel/idea/annotator/CamelSimpleAnnotatorTestIT.java
@@ -16,17 +16,6 @@
  */
 package org.apache.camel.idea.annotator;
 
-import java.io.File;
-
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.roots.ModuleRootModificationUtil;
-import com.intellij.openapi.roots.OrderRootType;
-import com.intellij.openapi.roots.libraries.Library;
-import com.intellij.openapi.roots.libraries.LibraryTable;
-import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.util.ui.UIUtil;
 import org.apache.camel.idea.CamelLightCodeInsightFixtureTestCaseIT;
 
 
@@ -35,25 +24,16 @@ import org.apache.camel.idea.CamelLightCodeInsightFixtureTestCaseIT;
  * TIP : Writing highlighting test can be tricky because if the highlight is one character off
  * it will fail, but the error messaged might still be correct. In this case it's likely the TextRange
  * is incorrect.
+ *
+ * So far we can have been able to avoid pointing the -Didea.home.path=<location of Intellij CI source code>
+ * because it's didn't really matter it could not resolve JDK classes when testing highlight. If you need
+ * to resolve the JDK classes you will have to point the idea.home.path to the right location
  */
 public class CamelSimpleAnnotatorTestIT extends CamelLightCodeInsightFixtureTestCaseIT {
 
-    public static final String CAMEL_CORE_MAVEN_ARTIFACT = "org.apache.camel:camel-core:2.19.0-SNAPSHOT";
-
     @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        File[] mavenArtifacts = getMavenArtifacts(CAMEL_CORE_MAVEN_ARTIFACT);
-        VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(mavenArtifacts[0]);
-        final LibraryTable projectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(myModule.getProject());
-        ApplicationManager.getApplication().runWriteAction(() -> {
-            Library library = projectLibraryTable.createLibrary("Maven: " + CAMEL_CORE_MAVEN_ARTIFACT);
-            final Library.ModifiableModel libraryModifiableModel = library.getModifiableModel();
-            libraryModifiableModel.addRoot(virtualFile, OrderRootType.CLASSES);
-            libraryModifiableModel.commit();
-            ModuleRootModificationUtil.addDependency(myModule, library);
-        });
-        UIUtil.dispatchAllInvocationEvents();
+    protected String getTestDataPath() {
+        return "src/test/resources/testData/annotator";
     }
 
     public void testAnnotatorSimpleValidation() {
@@ -61,6 +41,41 @@ public class CamelSimpleAnnotatorTestIT extends CamelLightCodeInsightFixtureTest
         myFixture.checkHighlighting(false, false, true, true);
     }
 
+    public void testAnnotatorExpressionValidation() {
+        myFixture.configureByText("AnnotatorTestData.java", getJavaWithExpression());
+        myFixture.checkHighlighting(false, false, true, true);
+    }
+
+    public void testAnnotatorOpenBracketSimpleValidation() {
+        myFixture.configureByText("AnnotatorTestData.java", getJavaOpenBracketWithSimple());
+        myFixture.checkHighlighting(false, false, true, true);
+    }
+
+    public void testAnnotatorMultipleOpenBracketSimpleValidation() {
+        myFixture.configureByText("AnnotatorTestData.java", getJavaMutlipleOpenBracketWithSimple());
+        myFixture.checkHighlighting(false, false, true, true);
+    }
+
+    public void testAnnotatorCamelPredicateValidation() {
+        myFixture.configureByText("AnnotatorTestData.java", getJavaWithCamelPredicate());
+        myFixture.checkHighlighting(false, false, false, true);
+    }
+
+    public void testAnnotatorCamelPredicateValidation2() {
+        myFixture.configureByText("AnnotatorTestData.java", getJavaWithCamelPredicate2());
+        myFixture.checkHighlighting(false, false, false, true);
+    }
+
+    public void testXmlAnnotatorSimpleValidation2() {
+        myFixture.configureByText("AnnotatorTestData.xml", getXmlWithSimple());
+        myFixture.checkHighlighting(false, false, false, true);
+    }
+
+    public void testXmlAnnotatorPredicateValidation2() {
+        myFixture.configureByText("AnnotatorTestData.xml", getXmlWithPredicate());
+        myFixture.checkHighlighting(false, false, false, true);
+    }
+
     private String getJavaWithSimple() {
         return "import org.apache.camel.builder.RouteBuilder;\\n"
             + "public class MyRouteBuilder extends RouteBuilder {\\n"
@@ -68,8 +83,98 @@ public class CamelSimpleAnnotatorTestIT extends CamelLightCodeInsightFixtureTest
             + "            from(\\"netty-http:http://localhost/cdi?matchOnUriPrefix=true&nettySharedHttpServer=#httpServer\\")\\n"
             + "            .id(\\"http-route-cdi\\")\\n"
             + "            .transform()\\n"
-            + "            .simple(\\"Response from Camel CDI on route<error descr=\\"Unknown function: xrouteId\\"> ${xrouteId} using thread: ${threadName}</error>\\");"
+            + "            .simple(\\"Response from Camel CDI on route<error descr=\\"Unknown function: xrouteId\\">${xrouteId}</error> using thread: ${threadName}\\");"
+            + "        }\\n"
+            + "    }";
+    }
+
+    private String getJavaWithExpression() {
+        return "import org.apache.camel.builder.RouteBuilder;"
+            + "public class MyRouteBuilder extends RouteBuilder {\\n"
+            + "    public void configure() throws Exception {\\n"
+            + "      from(\\"timer:stream?repeatCount=1\\")\\n"
+            + "           .log(\\"Result from query <error descr=\\"Unknown function: xbody\\">${xbody}</error>\\")\\n"
+            + "           .process(exchange -> {\\n"
+            + "                exchange.getIn().setBody(Arrays.asList(\\"fharms\\"));\\n"
+            + "           .to(\\"file:test.txt\\");\\n"
+            + "    }"
+            + " }";
+    }
+
+    private String getJavaOpenBracketWithSimple() {
+        return "import org.apache.camel.builder.RouteBuilder;\\n"
+            + "public class MyRouteBuilder extends RouteBuilder {\\n"
+            + "        public void configure() throws Exception {\\n"
+            + "            from(\\"netty-http:http://localhost/cdi?matchOnUriPrefix=true&nettySharedHttpServer=#httpServer\\")\\n"
+            + "            .id(\\"http-route-cdi\\")\\n"
+            + "            .transform()\\n"
+            + "            .simple(\\"Response from Camel CDI on route${routeId} using thread: ${threadNam<error descr=\\"expected symbol functionEnd but was eol\\">e</error>\\");"
+            + "        }\\n"
+            + "    }";
+    }
+
+    private String getJavaMutlipleOpenBracketWithSimple() {
+        return "import org.apache.camel.builder.RouteBuilder;\\n"
+            + "public class MyRouteBuilder extends RouteBuilder {\\n"
+            + "        public void configure() throws Exception {\\n"
+            + "            from(\\"netty-http:http://localhost/cdi?matchOnUriPrefix=true&nettySharedHttpServer=#httpServer\\")\\n"
+            + "            .id(\\"http-route-cdi\\")\\n"
+            + "            .transform()\\n"
+            + "            .simple(\\"Response from Camel CDI on route${routeId} using thread: ${threadNam<error descr=\\"expected symbol functionEnd but was eol\\">e</error>\\");"
+            + "        }\\n"
+            + "    }";
+    }
+
+    private String getJavaWithCamelPredicate() {
+        return "import org.apache.camel.builder.RouteBuilder;\\n"
+            + "public class MyRouteBuilder extends RouteBuilder {\\n"
+            + "        public void configure() throws Exception {\\n"
+            + "              from(\\"direct:start\\")\\n"
+            + "                .loopDoWhile(simple(\\"${body.length} <error descr=\\"Unexpected token =\\">=!=</error> 12\\"))\\n"
+            + "                .to(\\"mock:loop\\")\\n"
+            + "                .transform(body().append(\\"A\\"))\\n"
+            + "                .end()\\n"
+            + "                .to(\\"mock:result\\");"
             + "        }\\n"
             + "    }";
     }
+
+    private String getJavaWithCamelPredicate2() {
+        return "import org.apache.camel.builder.RouteBuilder;\\n"
+            + "public class MyRouteBuilder extends RouteBuilder {\\n"
+            + "        public void configure() throws Exception {\\n"
+            + "              from(\\"direct:start\\")\\n"
+            + "                .loopDoWhile(simple(\\"${body.length} != 12\\"))\\n"
+            + "                .filter().simple(<error descr=\\"Unexpected token x\\">\\"xxxx\\"</error>)\\n"
+            + "                .filter(simple(<error descr=\\"Unexpected token y\\">\\"yyyy\\"</error>))\\n"
+            + "                .to(\\"mock:loop\\")\\n"
+            + "                .transform(body().append(\\"A\\"))\\n"
+            + "                .end()\\n"
+            + "                .to(\\"mock:result\\");"
+            + "        }\\n"
+            + "    }";
+    }
+
+    private String getXmlWithSimple() {
+        return "<camelContext xmlns=\\"http://camel.apache.org/schema/spring\\">\\n"
+            + "  <route id=\\"timerToInRoute\\">\\n"
+            + "  <from uri=\\"timer:foo?period=1s\\"/>\\n"
+            + "  <transform>\\n"
+            + "  <simple>Message at <error descr=\\"Unknown function: daxcdte:now:yyyy-MM-dd HH:mm:ss\\">${daxcdte:now:yyyy-MM-dd HH:mm:ss}</error></simple>\\n"
+            + "  </transform>\\n"
+            + "  <to uri=\\"activemq:queue:inbox\\"/>\\n"
+            + "  </camelContext>";
+    }
+
+    private String getXmlWithPredicate() {
+        return "<from uri=\\"direct:start\\"/>\\n"
+            + "    <loop doWhile=\\"true\\">\\n"
+            + "      <simple>${body.length} !s1= 12</simple>\\n"
+            + "      <filter/><simple><error descr=\\"Unexpected token x\\">xxxx</error></simple>\\n"
+            + "      <filter>\\n"
+            + "        <simple><error descr=\\"Unexpected token y\\">yyyy</error></simple>\\n"
+            + "      </filter>\\n"
+            + "    </loop>\\n"
+            + "    <to>mock:result</to>";
+    }
 }
\\ No newline at end of file
diff --git a/camel-idea-plugin/src/test/java/org/apache/camel/idea/service/CamelCatalogServiceTestIT.java b/camel-idea-plugin/src/test/java/org/apache/camel/idea/service/CamelCatalogServiceTestIT.java
index ba02b1c..86e6940 100644
--- a/camel-idea-plugin/src/test/java/org/apache/camel/idea/service/CamelCatalogServiceTestIT.java
+++ b/camel-idea-plugin/src/test/java/org/apache/camel/idea/service/CamelCatalogServiceTestIT.java
@@ -26,8 +26,10 @@ import org.apache.camel.idea.CamelLightCodeInsightFixtureTestCaseIT;
  */
 public class CamelCatalogServiceTestIT extends CamelLightCodeInsightFixtureTestCaseIT {
 
+
     @Override
     protected void setUp() throws Exception {
+        setIgnoreCamelCoreLib(true);
         super.setUp();
         ServiceManager.getService(myModule.getProject(), CamelService.class).setCamelPresent(false);
     }