diff --git a/java/java-psi-api/src/com/intellij/psi/JavaTokenType.java b/java/java-psi-api/src/com/intellij/psi/JavaTokenType.java index 40c5837b58ae..d63ca45d842a 100644 --- a/java/java-psi-api/src/com/intellij/psi/JavaTokenType.java +++ b/java/java-psi-api/src/com/intellij/psi/JavaTokenType.java @@ -142,4 +142,8 @@ public interface JavaTokenType extends TokenType { IElementType VAR_KEYWORD = new IJavaElementType("VAR"); IElementType YIELD_KEYWORD = new IJavaElementType("YIELD"); IElementType RECORD_KEYWORD = new IJavaElementType("RECORD"); + + IElementType SEALED_KEYWORD = new IJavaElementType("SEALED"); + IElementType NON_SEALED_KEYWORD = new IKeywordElementType("NON_SEALED"); + IElementType PERMITS_KEYWORD = new IJavaElementType("PERMITS"); } \ No newline at end of file diff --git a/java/java-psi-api/src/com/intellij/psi/PsiKeyword.java b/java/java-psi-api/src/com/intellij/psi/PsiKeyword.java index 7210fcb31d04..1340a6a0fd7b 100644 --- a/java/java-psi-api/src/com/intellij/psi/PsiKeyword.java +++ b/java/java-psi-api/src/com/intellij/psi/PsiKeyword.java @@ -75,4 +75,8 @@ public interface PsiKeyword extends PsiJavaToken { String YIELD = "yield"; String RECORD = "record"; + + String SEALED = "sealed"; + String PERMITS = "permits"; + String NON_SEALED = "non-sealed"; } \ No newline at end of file diff --git a/java/java-psi-impl/src/com/intellij/lang/java/lexer/JavaLexer.java b/java/java-psi-impl/src/com/intellij/lang/java/lexer/JavaLexer.java index 886f357f4cd7..672d1871c2e2 100644 --- a/java/java-psi-impl/src/com/intellij/lang/java/lexer/JavaLexer.java +++ b/java/java-psi-impl/src/com/intellij/lang/java/lexer/JavaLexer.java @@ -25,12 +25,17 @@ public class JavaLexer extends LexerBase { ABSTRACT, BOOLEAN, BREAK, BYTE, CASE, CATCH, CHAR, CLASS, CONST, CONTINUE, DEFAULT, DO, DOUBLE, ELSE, EXTENDS, FINAL, FINALLY, FLOAT, FOR, GOTO, IF, IMPLEMENTS, IMPORT, INSTANCEOF, INT, INTERFACE, LONG, NATIVE, NEW, PACKAGE, PRIVATE, PROTECTED, PUBLIC, RETURN, SHORT, STATIC, STRICTFP, SUPER, SWITCH, SYNCHRONIZED, THIS, THROW, THROWS, TRANSIENT, TRY, VOID, VOLATILE, WHILE, - TRUE, FALSE, NULL); + TRUE, FALSE, NULL, NON_SEALED); private static final Set JAVA9_KEYWORDS = new THashSet<>(Arrays.asList(OPEN, MODULE, REQUIRES, EXPORTS, OPENS, USES, PROVIDES, TRANSITIVE, TO, WITH), CharSequenceHashingStrategy.CASE_SENSITIVE); + // Minus is a single token that may separate identifiers that can form "non-sealed" keyword + // For incremental lexing it is important that at "-" lexer is not in the initial state so we will lex one more token + private static final int INITIAL_STATE = 0; + private static final int MAYBE_NON_SEALED_STATE = -1; + public static boolean isKeyword(String id, @NotNull LanguageLevel level) { return KEYWORDS.contains(id) || level.isAtLeast(LanguageLevel.JDK_1_4) && ASSERT.equals(id) || @@ -41,19 +46,24 @@ public class JavaLexer extends LexerBase { return id != null && (level.isAtLeast(LanguageLevel.JDK_1_9) && JAVA9_KEYWORDS.contains(id) || level.isAtLeast(LanguageLevel.JDK_10) && VAR.contentEquals(id) || + level.isAtLeast(LanguageLevel.JDK_14_PREVIEW) && RECORD.contentEquals(id) || level.isAtLeast(LanguageLevel.JDK_14) && YIELD.contentEquals(id) || - level.isAtLeast(LanguageLevel.JDK_14_PREVIEW) && RECORD.contentEquals(id)); + (level.isAtLeast(LanguageLevel.JDK_15_PREVIEW) && (SEALED.contentEquals(id) || PERMITS.contentEquals(id))) + ); } private final _JavaLexer myFlexLexer; + private final LanguageLevel myLevel; private CharSequence myBuffer; private char @Nullable [] myBufferArray; private int myBufferIndex; private int myBufferEndOffset; private int myTokenEndOffset; // positioned after the last symbol of the current token private IElementType myTokenType; + private int myState = INITIAL_STATE; public JavaLexer(@NotNull LanguageLevel level) { + myLevel = level; myFlexLexer = new _JavaLexer(level); } @@ -70,7 +80,7 @@ public class JavaLexer extends LexerBase { @Override public int getState() { - return 0; + return myState; } @Override @@ -106,6 +116,7 @@ public class JavaLexer extends LexerBase { myBufferIndex = myTokenEndOffset; + boolean wasMinus = false; char c = charAt(myBufferIndex); switch (c) { case ' ': @@ -162,10 +173,36 @@ public class JavaLexer extends LexerBase { myTokenEndOffset = getClosingQuote(myBufferIndex + 1, c); } break; - + case 'n': + if (myLevel.isAtLeast(LanguageLevel.JDK_15_PREVIEW) && + myBufferIndex + 9 < myBufferEndOffset && + charAt(myBufferIndex + 1) == 'o' && + charAt(myBufferIndex + 2) == 'n' && + charAt(myBufferIndex + 3) == '-' && + charAt(myBufferIndex + 4) == 's' && + charAt(myBufferIndex + 5) == 'e' && + charAt(myBufferIndex + 6) == 'a' && + charAt(myBufferIndex + 7) == 'l' && + charAt(myBufferIndex + 8) == 'e' && + charAt(myBufferIndex + 9) == 'd') { + myTokenType = JavaTokenType.NON_SEALED_KEYWORD; + myTokenEndOffset = myBufferIndex + 10; + } + else { + flexLocateToken(); + } + break; + case '-': + wasMinus = true; + // fallthrough default: flexLocateToken(); } + if (wasMinus) { + myState = MAYBE_NON_SEALED_STATE; + } else { + myState = INITIAL_STATE; + } if (myTokenEndOffset > myBufferEndOffset) { myTokenEndOffset = myBufferEndOffset; diff --git a/java/java-tests/testSrc/com/intellij/java/editor/JavaHighlighterTest.java b/java/java-tests/testSrc/com/intellij/java/editor/JavaHighlighterTest.java index 418936fe252e..f3ad6a0dfdb2 100644 --- a/java/java-tests/testSrc/com/intellij/java/editor/JavaHighlighterTest.java +++ b/java/java-tests/testSrc/com/intellij/java/editor/JavaHighlighterTest.java @@ -12,6 +12,7 @@ import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.highlighter.EditorHighlighter; import com.intellij.openapi.editor.highlighter.HighlighterIterator; +import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.JavaDocTokenType; import com.intellij.psi.JavaTokenType; import com.intellij.testFramework.LightJavaCodeInsightTestCase; @@ -24,6 +25,11 @@ public class JavaHighlighterTest extends LightJavaCodeInsightTestCase { private Document myDocument; private final ArrayList myEditorsToRelease = new ArrayList<>(); + @Override + protected LanguageLevel getLanguageLevel() { + return LanguageLevel.JDK_15_PREVIEW; + } + @Override protected void tearDown() throws Exception { try { @@ -158,6 +164,23 @@ public class JavaHighlighterTest extends LightJavaCodeInsightTestCase { CheckHighlighterConsistency.performCheck(editor); } + public void testNonSealedEditing() { + Editor editor = initDocument("non-sealed class A {\n" + + " \n" + + "} "); + WriteCommandAction.runWriteCommandAction(getProject(), () -> { + int idx = myDocument.getText().lastIndexOf(" class"); + myDocument.deleteString(idx - 1, idx); + }); + CheckHighlighterConsistency.performCheck(editor); + + WriteCommandAction.runWriteCommandAction(getProject(), () -> { + int idx = myDocument.getText().lastIndexOf(" class"); + myDocument.insertString(idx, "d"); + }); + CheckHighlighterConsistency.performCheck(editor); + } + private Editor initDocument(String text) { EditorFactory editorFactory = EditorFactory.getInstance(); myDocument = editorFactory.createDocument(text); @@ -165,7 +188,7 @@ public class JavaHighlighterTest extends LightJavaCodeInsightTestCase { myHighlighter = HighlighterFactory .createHighlighter(JavaFileType.INSTANCE, EditorColorsManager.getInstance().getGlobalScheme(), getProject()); - ((EditorEx) editor).setHighlighter(myHighlighter); + ((EditorEx)editor).setHighlighter(myHighlighter); myEditorsToRelease.add(editor); return editor; diff --git a/java/java-tests/testSrc/com/intellij/java/lexer/Java15LexerTest.java b/java/java-tests/testSrc/com/intellij/java/lexer/Java15LexerTest.java new file mode 100644 index 000000000000..078867f46f52 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/lexer/Java15LexerTest.java @@ -0,0 +1,23 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.java.lexer; + +import com.intellij.lang.java.JavaParserDefinition; +import com.intellij.lexer.Lexer; +import com.intellij.pom.java.LanguageLevel; +import com.intellij.testFramework.LexerTestCase; + +public class Java15LexerTest extends LexerTestCase { + public void testNonSealed() { + doTest("non-sealed", "NON_SEALED ('non-sealed')"); + } + + @Override + protected Lexer createLexer() { + return JavaParserDefinition.createLexer(LanguageLevel.JDK_15_PREVIEW); + } + + @Override + protected String getDirPath() { + return ""; + } +} \ No newline at end of file