regexp: first pass at validation and completion for jdk 9 named characters (IDEA...
authorBas Leijdekkers <basleijdekkers@gmail.com>
Mon, 29 Aug 2016 18:49:51 +0000 (20:49 +0200)
committerBas Leijdekkers <basleijdekkers@gmail.com>
Mon, 29 Aug 2016 18:54:00 +0000 (20:54 +0200)
RegExpSupport/src/org/intellij/lang/regexp/AsciiUtil.java
RegExpSupport/src/org/intellij/lang/regexp/RegExpCompletionContributor.java
RegExpSupport/src/org/intellij/lang/regexp/UnicodeCharacterNames.java [new file with mode: 0644]
RegExpSupport/src/org/intellij/lang/regexp/validation/RegExpAnnotator.java
java/java-impl/src/com/intellij/psi/impl/JavaRegExpHost.java

index c0201ee9b9f464d1f27259a3734f3243fcfaa44a..83757f674569454b8c19dfdef9e3a80843a6ef98 100644 (file)
  */
 package org.intellij.lang.regexp;
 
+import java.nio.charset.Charset;
+
 /**
  * @author Bas Leijdekkers
  */
 public final class AsciiUtil {
 
+  public static final Charset ASCII_CHARSET = Charset.forName("US-ASCII");
+
   private AsciiUtil() {}
 
   public static boolean isUpperCase(char c) {
index 7b9461a10829b334e22d371e918e5ae6bc8d2090..dc81e12c3030b7d569163a5bb0d89fc07de77233 100644 (file)
@@ -22,8 +22,10 @@ import com.intellij.codeInsight.lookup.LookupElementBuilder;
 import com.intellij.codeInsight.lookup.TailTypeDecorator;
 import com.intellij.openapi.editor.Document;
 import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.progress.ProgressManager;
 import com.intellij.openapi.util.TextRange;
 import com.intellij.patterns.ElementPattern;
+import com.intellij.patterns.PsiElementPattern;
 import com.intellij.psi.PsiElement;
 import com.intellij.util.PlatformIcons;
 import com.intellij.util.ProcessingContext;
@@ -44,6 +46,12 @@ public final class RegExpCompletionContributor extends CompletionContributor {
 
   public RegExpCompletionContributor() {
     {
+      final PsiElementPattern.Capture<PsiElement> namedCharacterPattern = psiElement().withText("\\N");
+      extend(CompletionType.BASIC, psiElement().afterLeaf(namedCharacterPattern),
+             new NamedCharacterCompletionProvider(true));
+      extend(CompletionType.BASIC, psiElement().afterLeaf(psiElement(RegExpTT.LBRACE).afterLeaf(namedCharacterPattern)),
+             new NamedCharacterCompletionProvider(false));
+
       extend(CompletionType.BASIC, psiElement().withText("\\I"), new CharacterClassesNameCompletionProvider());
 
       final ElementPattern<PsiElement> propertyPattern = psiElement().withText("\\p");
@@ -60,6 +68,12 @@ public final class RegExpCompletionContributor extends CompletionContributor {
 
     {
       // TODO: backSlashPattern is needed for reg exp in injected context, remove when unescaping will be performed by Injecting framework
+      final PsiElementPattern.Capture<PsiElement> namedCharacterPattern = psiElement().withText("\\\\N");
+      extend(CompletionType.BASIC, psiElement().afterLeaf(namedCharacterPattern),
+             new NamedCharacterCompletionProvider(true));
+      extend(CompletionType.BASIC, psiElement().afterLeaf(psiElement(RegExpTT.LBRACE).afterLeaf(namedCharacterPattern)),
+             new NamedCharacterCompletionProvider(false));
+
       final ElementPattern<PsiElement> backSlashPattern = psiElement().withText("\\\\I");
       extend(CompletionType.BASIC, backSlashPattern, new CharacterClassesNameCompletionProvider());
 
@@ -124,6 +138,7 @@ public final class RegExpCompletionContributor extends CompletionContributor {
 
   private static class PropertyNameCompletionProvider extends CompletionProvider<CompletionParameters> {
 
+    @Override
     public void addCompletions(@NotNull final CompletionParameters parameters,
                                final ProcessingContext context,
                                @NotNull final CompletionResultSet result) {
@@ -136,6 +151,7 @@ public final class RegExpCompletionContributor extends CompletionContributor {
 
   private static class PropertyCompletionProvider extends CompletionProvider<CompletionParameters> {
 
+    @Override
     public void addCompletions(@NotNull final CompletionParameters parameters,
                                final ProcessingContext context,
                                @NotNull final CompletionResultSet result) {
@@ -147,6 +163,7 @@ public final class RegExpCompletionContributor extends CompletionContributor {
 
   private static class CharacterClassesNameCompletionProvider extends CompletionProvider<CompletionParameters> {
 
+    @Override
     public void addCompletions(@NotNull final CompletionParameters parameters,
                                final ProcessingContext context,
                                @NotNull final CompletionResultSet result)
@@ -160,4 +177,31 @@ public final class RegExpCompletionContributor extends CompletionContributor {
       }
     }
   }
+
+  private static class NamedCharacterCompletionProvider extends CompletionProvider<CompletionParameters> {
+
+    private final boolean myEmbrace;
+
+    public NamedCharacterCompletionProvider(boolean embrace) {
+      myEmbrace = embrace;
+    }
+
+    @Override
+    protected void addCompletions(@NotNull CompletionParameters parameters,
+                                  ProcessingContext context,
+                                  @NotNull CompletionResultSet result) {
+      UnicodeCharacterNames.iterate(name -> {
+        if (result.getPrefixMatcher().prefixMatches(name)) {
+          final String type = new String(new int[] {UnicodeCharacterNames.getCodePoint(name)}, 0, 1);
+          if (myEmbrace) {
+            result.addElement(createLookupElement("{" + name + "}", type, emptyIcon));
+          }
+          else {
+            result.addElement(TailTypeDecorator.withTail(createLookupElement(name, type, emptyIcon), TailType.createSimpleTailType('}')));
+          }
+        }
+        ProgressManager.checkCanceled();
+      });
+    }
+  }
 }
diff --git a/RegExpSupport/src/org/intellij/lang/regexp/UnicodeCharacterNames.java b/RegExpSupport/src/org/intellij/lang/regexp/UnicodeCharacterNames.java
new file mode 100644 (file)
index 0000000..57a4651
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.intellij.lang.regexp;
+
+import com.intellij.util.ReflectionUtil;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.function.Consumer;
+
+/**
+ * @author Bas Leijdekkers
+ */
+public class UnicodeCharacterNames {
+
+  public static void iterate(Consumer<String> consumer) {
+    try {
+      final Class<?> aClass = Class.forName("java.lang.CharacterName");
+      final Method initNamePool = ReflectionUtil.getDeclaredMethod(aClass, "initNamePool");
+      final int[][] lookup2d = ReflectionUtil.getField(aClass, null, int[][].class, "lookup");
+      if (initNamePool != null && lookup2d != null) { // jdk 8
+        byte[] namePool = (byte[])initNamePool.invoke(null);
+        for (int[] indexes : lookup2d) {
+          if (indexes != null) {
+            for (int index : indexes) {
+              if (index != 0) {
+                ;
+                final String name = new String(namePool, index >>> 8, index & 0xff, AsciiUtil.ASCII_CHARSET);
+                consumer.accept(name);
+              }
+            }
+          }
+        }
+      }
+      else {
+        final Method instance = ReflectionUtil.getDeclaredMethod(aClass, "getInstance");
+        final Field field1 = ReflectionUtil.getDeclaredField(aClass, "strPool");
+        final Field field2 = ReflectionUtil.getDeclaredField(aClass, "lookup");
+        if (instance != null && field1 != null && field2 != null) { // jdk 9
+          final Object characterName = instance.invoke(null);
+          byte[] namePool = (byte[])field1.get(characterName);
+          final int[] lookup = (int[])field2.get(characterName);
+          for (int index : lookup) {
+            if (index != 0) {
+              final String name = new String(namePool, index >>> 8, index & 0xff, AsciiUtil.ASCII_CHARSET);
+              consumer.accept(name);
+            }
+          }
+        }
+      }
+    }
+    catch (ClassNotFoundException | InvocationTargetException | IllegalAccessException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static int getCodePoint(String name) {
+    if (name == null) {
+      return -1;
+    }
+    final Method method = ReflectionUtil.getMethod(Character.class, "codePointOf", String.class); // jdk 9 method
+    if (method != null) {
+      try {
+        return (int)method.invoke(null, name);
+      }
+      catch (IllegalArgumentException e) {
+        return -1;
+      }
+      catch (IllegalAccessException | InvocationTargetException e) {
+        throw new RuntimeException(e);
+      }
+    }
+    try {
+      // jdk 8 fallback
+      final Class<?> aClass = Class.forName("java.lang.CharacterName");
+      final Method initNamePool = ReflectionUtil.getDeclaredMethod(aClass, "initNamePool");
+      if (initNamePool == null) {
+        return -1; // give up
+      }
+      byte[] namePool = (byte[])initNamePool.invoke(null);
+      name = name.trim().toUpperCase(Locale.ROOT);
+      byte[] key = name.getBytes(StandardCharsets.ISO_8859_1);
+      final int[][] lookup = ReflectionUtil.getField(aClass, null, int[][].class, "lookup");
+      if (lookup == null) throw new RuntimeException();
+      for (int i = 0; i < lookup.length; i++) {
+        int[] indexes = lookup[i];
+        if (indexes != null) {
+          for (int j = 0; j < indexes.length; j++) {
+            int index = indexes[j];
+            if ((index & 0xFF) == key.length && matches(namePool, index >>> 8, key)) {
+              return i << 8 | j;
+            }
+          }
+        }
+      }
+      return getUnnamedUnicodeCharacterCodePoint(name);
+    }
+    catch (ClassNotFoundException | InvocationTargetException | IllegalAccessException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static int getUnnamedUnicodeCharacterCodePoint(String name) {
+    int index = name.lastIndexOf(' ');
+    if (index != -1) {
+      try {
+        int c = Integer.parseInt(name.substring(index + 1, name.length()), 16);
+        if (Character.isValidCodePoint(c) && name.equals(Character.getName(c))) return c;
+      }
+      catch (NumberFormatException ignore) {
+      }
+    }
+    return -1;
+  }
+
+  private static boolean matches(byte[] bytes, int offset, byte[] key) {
+    if (offset < 0 || offset + key.length > bytes.length) {
+      throw new IllegalArgumentException();
+    }
+    for (int i = 0; i < key.length; i++) {
+      if (bytes[i + offset] != key[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
index b77107267b300ca69d7db2e0ebb5b2f893966f19..3618b81df854b4bd8323c9c7379752ea7174ea9c 100644 (file)
@@ -232,8 +232,11 @@ public final class RegExpAnnotator extends RegExpElementVisitor implements Annot
       myHolder.createErrorAnnotation(namedCharacter, "Named Unicode characters are not allowed in this regular expression dialect");
     }
     else if (!myLanguageHosts.isValidNamedCharacter(namedCharacter)) {
-      final Annotation a = myHolder.createErrorAnnotation(namedCharacter, "Unknown character name");
-      a.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
+      final ASTNode node = namedCharacter.getNameNode();
+      if (node != null) {
+        final Annotation a = myHolder.createErrorAnnotation(node, "Unknown character name");
+        a.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
+      }
     }
   }
 
index 5da1e23b1d85f0c0dd2c15f486d099208009eed5..f2e1e23340f2e6b5cd761d244b4b0f36c1205e7a 100644 (file)
@@ -24,6 +24,7 @@ import com.intellij.openapi.roots.ModuleRootManager;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.psi.PsiElement;
 import org.intellij.lang.regexp.AsciiUtil;
+import org.intellij.lang.regexp.UnicodeCharacterNames;
 import org.intellij.lang.regexp.DefaultRegExpPropertiesProvider;
 import org.intellij.lang.regexp.RegExpLanguageHost;
 import org.intellij.lang.regexp.psi.*;
@@ -322,6 +323,11 @@ public class JavaRegExpHost implements RegExpLanguageHost {
     }
   }
 
+  @Override
+  public boolean isValidNamedCharacter(RegExpNamedCharacter namedCharacter) {
+    return UnicodeCharacterNames.getCodePoint(namedCharacter.getName()) >= 0;
+  }
+
   @NotNull
   @Override
   public String[][] getAllKnownProperties() {