idea插件开发-自定义语言4-Syntax Highlighter

SyntaxHighlighter用于指定应如何突出显示特定范围的文本,ColorSettingPage可以定义颜色。

一、Syntax Highter

1、文本属性键

TextAttributesKey用于指定应如何突出显示特定范围的文本。不同类型的数据比如关键字、数字、字符串等如果要突出显示都需要创建一个TextAttributesKey实例。一个类型如果拥有多个TextAttributesKey突出显示时可以分层------例如,一个键可以定义类型的粗体和另一种颜色。

2、颜色设置

EditorColorsScheme用来定义一个编辑器要使用哪些TextAttributesKey。这些是可以通过Settings | Editor | Color Scheme 来设置的。可通过com.intellij.colorSettingsPage进行扩展。

另外File | Export | Files or Selection to HTML 也采用了同样的配色方案。颜色设置可以参考示例:

java 复制代码
final class PropertiesColorsPage implements ColorSettingsPage {
  private static final AttributesDescriptor[] ATTRS;

  static {
    ATTRS = Arrays.stream(PropertiesComponent.values())
      .map(component -> new AttributesDescriptor(component.getMessagePointer(), component.getTextAttributesKey()))
      .toArray(AttributesDescriptor[]::new)
    ;
  }

  @Override
  @NotNull
  public String getDisplayName() {
    return OptionsBundle.message("properties.options.display.name");
  }

  @Override
  public Icon getIcon() {
    return AllIcons.FileTypes.Properties;
  }

  @Override
  public AttributesDescriptor @NotNull [] getAttributeDescriptors() {
    return ATTRS;
  }

  @Override
  public ColorDescriptor @NotNull [] getColorDescriptors() {
    return ColorDescriptor.EMPTY_ARRAY;
  }

  @Override
  @NotNull
  public SyntaxHighlighter getHighlighter() {
    return new PropertiesHighlighter();
  }

  @Override
  @NotNull
  public String getDemoText() {
    return """
      # This comment starts with '#'
      greetings=Hello
      ! This comment starts with '!'
      what\\=to\\=greet : \\'W\\o\\rld\\',\\tUniverse\\n\\uXXXX
      """
      ;
  }

  @Override
  public Map<String, TextAttributesKey> getAdditionalHighlightingTagToDescriptorMap() {
    return null;
  }
}

3、词法分析器

SyntaxHighlighter提供了第一级的语法高亮级别功能,语法高亮器返回TextAttributesKey每个标记类型的实例都可以特别高亮显示。可参考示例:

java 复制代码
public class PropertiesHighlighter extends SyntaxHighlighterBase {
  @Override
  @NotNull
  public Lexer getHighlightingLexer() {
    return new PropertiesHighlightingLexer();
  }

  @Override
  public TextAttributesKey @NotNull [] getTokenHighlights(IElementType tokenType) {
    final PropertiesComponent type = PropertiesComponent.getByTokenType(tokenType);

    TextAttributesKey key = null;
    if (type != null) {
      key = type.getTextAttributesKey();
    }

    return SyntaxHighlighterBase.pack(key);
  }

  public enum PropertiesComponent {
    PROPERTY_KEY(
      TextAttributesKey.createTextAttributesKey("PROPERTIES.KEY", DefaultLanguageHighlighterColors.KEYWORD),
      PropertiesBundle.messagePointer("options.properties.attribute.descriptor.property.key"),
      PropertiesTokenTypes.KEY_CHARACTERS
    ),
    PROPERTY_VALUE(
      TextAttributesKey.createTextAttributesKey("PROPERTIES.VALUE", DefaultLanguageHighlighterColors.STRING),
      PropertiesBundle.messagePointer("options.properties.attribute.descriptor.property.value"),
      PropertiesTokenTypes.VALUE_CHARACTERS
    ),
    PROPERTY_COMMENT(
      TextAttributesKey.createTextAttributesKey("PROPERTIES.LINE_COMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT),
      PropertiesBundle.messagePointer("options.properties.attribute.descriptor.comment"),
      PropertiesTokenTypes.END_OF_LINE_COMMENT
    ),
    PROPERTY_KEY_VALUE_SEPARATOR(
      TextAttributesKey.createTextAttributesKey("PROPERTIES.KEY_VALUE_SEPARATOR", DefaultLanguageHighlighterColors.OPERATION_SIGN),
      PropertiesBundle.messagePointer("options.properties.attribute.descriptor.key.value.separator"),
      PropertiesTokenTypes.KEY_VALUE_SEPARATOR
    ),
    PROPERTIES_VALID_STRING_ESCAPE(
      TextAttributesKey.createTextAttributesKey("PROPERTIES.VALID_STRING_ESCAPE", DefaultLanguageHighlighterColors.VALID_STRING_ESCAPE),
      PropertiesBundle.messagePointer("options.properties.attribute.descriptor.valid.string.escape"),
      StringEscapesTokenTypes.VALID_STRING_ESCAPE_TOKEN
    ),
    PROPERTIES_INVALID_STRING_ESCAPE(
      TextAttributesKey.createTextAttributesKey("PROPERTIES.INVALID_STRING_ESCAPE", DefaultLanguageHighlighterColors.INVALID_STRING_ESCAPE),
      PropertiesBundle.messagePointer("options.properties.attribute.descriptor.invalid.string.escape"),
      StringEscapesTokenTypes.INVALID_UNICODE_ESCAPE_TOKEN
    );

    private static final Map<IElementType, PropertiesComponent> elementTypeToComponent;
    private static final Map<TextAttributesKey, PropertiesComponent> textAttributeKeyToComponent;

    static {
      elementTypeToComponent = Arrays.stream(values())
        .collect(Collectors.toMap(PropertiesComponent::getTokenType, Function.identity()));

      textAttributeKeyToComponent = Arrays.stream(values())
        .collect(Collectors.toMap(PropertiesComponent::getTextAttributesKey, Function.identity()));
    }

    private final TextAttributesKey myTextAttributesKey;
    private final Supplier<@Nls String> myMessagePointer;
    private final IElementType myTokenType;

    PropertiesComponent(TextAttributesKey textAttributesKey, Supplier<@Nls String> messagePointer, IElementType tokenType) {
      myTextAttributesKey = textAttributesKey;
      myMessagePointer = messagePointer;
      myTokenType = tokenType;
    }

    public TextAttributesKey getTextAttributesKey() {
      return myTextAttributesKey;
    }

    Supplier<@Nls String> getMessagePointer() {
      return myMessagePointer;
    }

    IElementType getTokenType() {
      return myTokenType;
    }

    static PropertiesComponent getByTokenType(IElementType tokenType) {
      return elementTypeToComponent.get(tokenType);
    }

    static PropertiesComponent getByTextAttribute(TextAttributesKey textAttributesKey) {
      return textAttributeKeyToComponent.get(textAttributesKey);
    }

    static @Nls String getDisplayName(TextAttributesKey key) {
      final PropertiesComponent component = getByTextAttribute(key);
      if (component == null) return null;
      return component.getMessagePointer().get();
    }

    static @Nls HighlightSeverity getSeverity(TextAttributesKey key) {
      final PropertiesComponent component = getByTextAttribute(key);
      return component == PROPERTIES_INVALID_STRING_ESCAPE
             ? HighlightSeverity.WARNING
             : null;
    }
  }

}

语义高亮

语义突出显示其实是提供了一个额外的着色层,以改善几个相关项(例如,方法参数、局部变量)的视觉区分。可实现com.intellij.highlightVisitor扩展点,这里的颜色设置要实现``RainbowColorSettingsPage接口。

4、解析器

第二级错误突出显示发生在解析期间。如果根据语言的语法,特定的标记序列是无效的,则PsiBuilder.error()方法可以突出显示无效标记并显示一条错误消息,说明它们无效的原因。

5、注释器

第三层高亮是通过Annotator接口实现。一个插件可以在扩展点中注册一个或多个注释器com.intellij.annotator,这些注释器在后台高亮过程中被调用,以处理自定义语言的 PSI 树中的元素。属性language应设置为此注释器适用的语言 ID。

注释器不仅可以分析语法,还可以用于 PSI 分析语义,因此可以提供更复杂的语法和错误突出显示逻辑。注释器还可以为其检测到的问题提供快速修复。文件更改时,将增量调用注释器以仅处理 PSI 树中更改的元素。

错误/警告

大体的代码实现如下:

java 复制代码
holder.newAnnotation(HighlightSeverity.WARNING,"Invalid code") // or HighlightSeverity.ERROR
    .withFix(new MyFix(psiElement))
    .create();

句法

大体的代码实现如下:

java 复制代码
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
    .range(rangeToHighlight)
    .textAttributes(MyHighlighter.EXTRA_HIGHLIGHT_ATTRIBUTE)
    .create();

完整的示例可参考:

java 复制代码
public class PropertiesAnnotator implements Annotator {
  private static final ExtensionPointName<DuplicatePropertyKeyAnnotationSuppressor>
    EP_NAME = ExtensionPointName.create("com.intellij.properties.duplicatePropertyKeyAnnotationSuppressor");

  @Override
  public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
    if (!(element instanceof Property property)) return;
    PropertiesFile propertiesFile = property.getPropertiesFile();
    final String key = property.getUnescapedKey();
    if (key == null) return;
    Collection<IProperty> others = propertiesFile.findPropertiesByKey(key);
    ASTNode keyNode = ((PropertyImpl)property).getKeyNode();
    if (keyNode == null) return;
    if (others.size() != 1 &&
      EP_NAME.findFirstSafe(suppressor -> suppressor.suppressAnnotationFor(property)) == null) {
      holder.newAnnotation(HighlightSeverity.ERROR,PropertiesBundle.message("duplicate.property.key.error.message")).range(keyNode)
      .withFix(PropertiesQuickFixFactory.getInstance().createRemovePropertyFix(property)).create();
    }

    highlightTokens(property, keyNode, holder, new PropertiesHighlighter());
    ASTNode valueNode = ((PropertyImpl)property).getValueNode();
    if (valueNode != null) {
      highlightTokens(property, valueNode, holder, new PropertiesValueHighlighter());
    }
  }

  private static void highlightTokens(final Property property, final ASTNode node, final AnnotationHolder holder, PropertiesHighlighter highlighter) {
    Lexer lexer = highlighter.getHighlightingLexer();
    final String s = node.getText();
    lexer.start(s);

    while (lexer.getTokenType() != null) {
      IElementType elementType = lexer.getTokenType();
      TextAttributesKey[] keys = highlighter.getTokenHighlights(elementType);
      for (TextAttributesKey key : keys) {
        final String displayName = PropertiesComponent.getDisplayName(key);
        final HighlightSeverity severity = PropertiesComponent.getSeverity(key);
        if (severity != null && displayName != null) {
          int start = lexer.getTokenStart() + node.getTextRange().getStartOffset();
          int end = lexer.getTokenEnd() + node.getTextRange().getStartOffset();
          TextRange textRange = new TextRange(start, end);
          TextAttributes attributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(key);
          AnnotationBuilder builder = holder.newAnnotation(severity, displayName).range(textRange).enforcedTextAttributes(attributes);

          int startOffset = textRange.getStartOffset();
          if (key == PropertiesComponent.PROPERTIES_INVALID_STRING_ESCAPE.getTextAttributesKey()) {
            builder = builder.withFix(new IntentionAction() {
              @Override
              @NotNull
              public String getText() {
                return PropertiesBundle.message("unescape");
              }

              @Override
              @NotNull
              public String getFamilyName() {
                return getText();
              }

              @Override
              public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
                if (!BaseIntentionAction.canModify(file)) return false;

                String text = file.getText();
                return text.length() > startOffset && text.charAt(startOffset) == '\\';
              }

              @Override
              public void invoke(@NotNull Project project, Editor editor, PsiFile file) {
                if (file.getText().charAt(startOffset) == '\\') {
                  editor.getDocument().deleteString(startOffset, startOffset + 1);
                }
              }

              @Override
              public boolean startInWriteAction() {
                return true;
              }
            });
          }
          builder.create();
        }
      }
      lexer.advance();
    }
  }
}

二、示例

1、定义SyntaxHighlighterFactory

java 复制代码
public class SimpleSyntaxHighlighter extends SyntaxHighlighterBase {

  public static final TextAttributesKey SEPARATOR =
          createTextAttributesKey("SIMPLE_SEPARATOR", DefaultLanguageHighlighterColors.OPERATION_SIGN);
  public static final TextAttributesKey KEY =
          createTextAttributesKey("SIMPLE_KEY", DefaultLanguageHighlighterColors.KEYWORD);
  public static final TextAttributesKey VALUE =
          createTextAttributesKey("SIMPLE_VALUE", DefaultLanguageHighlighterColors.STRING);
  public static final TextAttributesKey COMMENT =
          createTextAttributesKey("SIMPLE_COMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT);
  public static final TextAttributesKey BAD_CHARACTER =
          createTextAttributesKey("SIMPLE_BAD_CHARACTER", HighlighterColors.BAD_CHARACTER);


  private static final TextAttributesKey[] BAD_CHAR_KEYS = new TextAttributesKey[]{BAD_CHARACTER};
  private static final TextAttributesKey[] SEPARATOR_KEYS = new TextAttributesKey[]{SEPARATOR};
  private static final TextAttributesKey[] KEY_KEYS = new TextAttributesKey[]{KEY};
  private static final TextAttributesKey[] VALUE_KEYS = new TextAttributesKey[]{VALUE};
  private static final TextAttributesKey[] COMMENT_KEYS = new TextAttributesKey[]{COMMENT};
  private static final TextAttributesKey[] EMPTY_KEYS = new TextAttributesKey[0];

  @NotNull
  @Override
  public Lexer getHighlightingLexer() {
    return new SimpleLexerAdapter();
  }

  @Override
  public TextAttributesKey @NotNull [] getTokenHighlights(IElementType tokenType) {
    if (tokenType.equals(SimpleTypes.SEPARATOR)) {
      return SEPARATOR_KEYS;
    }
    if (tokenType.equals(SimpleTypes.KEY)) {
      return KEY_KEYS;
    }
    if (tokenType.equals(SimpleTypes.VALUE)) {
      return VALUE_KEYS;
    }
    if (tokenType.equals(SimpleTypes.COMMENT)) {
      return COMMENT_KEYS;
    }
    if (tokenType.equals(TokenType.BAD_CHARACTER)) {
      return BAD_CHAR_KEYS;
    }
    return EMPTY_KEYS;
  }

}
XML 复制代码
<extensions defaultExtensionNs="com.intellij">
  <lang.syntaxHighlighterFactory
      language="Simple"
      implementationClass="org.intellij.sdk.language.SimpleSyntaxHighlighterFactory"/>
</extensions>

2、定义颜色设置界面

java 复制代码
public class SimpleColorSettingsPage implements ColorSettingsPage {

  private static final AttributesDescriptor[] DESCRIPTORS = new AttributesDescriptor[]{
          new AttributesDescriptor("Key", SimpleSyntaxHighlighter.KEY),
          new AttributesDescriptor("Separator", SimpleSyntaxHighlighter.SEPARATOR),
          new AttributesDescriptor("Value", SimpleSyntaxHighlighter.VALUE),
          new AttributesDescriptor("Bad value", SimpleSyntaxHighlighter.BAD_CHARACTER)
  };

  @Nullable
  @Override
  public Icon getIcon() {
    return SimpleIcons.FILE;
  }

  @NotNull
  @Override
  public SyntaxHighlighter getHighlighter() {
    return new SimpleSyntaxHighlighter();
  }

  @NotNull
  @Override
  public String getDemoText() {
    return "# You are reading the \".properties\" entry.\n" +
            "! The exclamation mark can also mark text as comments.\n" +
            "website = https://en.wikipedia.org/\n" +
            "language = English\n" +
            "# The backslash below tells the application to continue reading\n" +
            "# the value onto the next line.\n" +
            "message = Welcome to \\\n" +
            "          Wikipedia!\n" +
            "# Add spaces to the key\n" +
            "key\\ with\\ spaces = This is the value that could be looked up with the key \"key with spaces\".\n" +
            "# Unicode\n" +
            "tab : \\u0009";
  }

  @Nullable
  @Override
  public Map<String, TextAttributesKey> getAdditionalHighlightingTagToDescriptorMap() {
    return null;
  }

  @Override
  public AttributesDescriptor @NotNull [] getAttributeDescriptors() {
    return DESCRIPTORS;
  }

  @Override
  public ColorDescriptor @NotNull [] getColorDescriptors() {
    return ColorDescriptor.EMPTY_ARRAY;
  }

  @NotNull
  @Override
  public String getDisplayName() {
    return "Simple";
  }

}

支持通过用 分隔节点来对运算符或大括号等相关属性进行分组//,例如:

java 复制代码
AttributesDescriptor[] DESCRIPTORS = new AttributesDescriptor[] {
    new AttributesDescriptor("Operators//Plus", MySyntaxHighlighter.PLUS),
    new AttributesDescriptor("Operators//Minus", MySyntaxHighlighter.MINUS),
    new AttributesDescriptor("Operators//Advanced//Sigma", MySyntaxHighlighter.SIGMA),
    new AttributesDescriptor("Operators//Advanced//Pi", MySyntaxHighlighter.PI),
    //...
};
XML 复制代码
<extensions defaultExtensionNs="com.intellij">
  <colorSettingsPage
      implementation="org.intellij.sdk.language.SimpleColorSettingsPage"/>
</extensions>

3、测试运行

颜色配置页面可从Settings | Editor | Color Scheme | Simple查看。如下图:

相关推荐
yanjiaweiya25 分钟前
云原生-集群管理
java·开发语言·云原生
gadiaola34 分钟前
【JavaSE面试篇】Java集合部分高频八股汇总
java·面试
艾迪的技术之路1 小时前
redisson使用lock导致死锁问题
java·后端·面试
今天背单词了吗9801 小时前
算法学习笔记:8.Bellman-Ford 算法——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·开发语言·后端·算法·最短路径问题
天天摸鱼的java工程师1 小时前
使用 Spring Boot 整合高德地图实现路线规划功能
java·后端
东阳马生架构2 小时前
订单初版—2.生单链路中的技术问题说明文档
java
咖啡啡不加糖2 小时前
暴力破解漏洞与命令执行漏洞
java·后端·web安全
风象南2 小时前
SpringBoot敏感配置项加密与解密实战
java·spring boot·后端
DKPT2 小时前
Java享元模式实现方式与应用场景分析
java·笔记·学习·设计模式·享元模式