From 8de3f13e006028f08d9ec39c609a45bbb034612c Mon Sep 17 00:00:00 2001 From: nbauma109 Date: Wed, 15 Apr 2026 09:22:57 +0200 Subject: [PATCH 01/19] Highlight keywords conservatively --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 146 ++++++++++++++++++ .../rsyntaxtextarea/modes/JavaTokenMaker.java | 146 ++++++++++++++++++ .../modes/JavaTokenMakerTest.java | 110 +++++++++++-- 3 files changed, 385 insertions(+), 17 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 0c5c81f3e..1c581813a 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -72,6 +72,147 @@ import org.fife.ui.rsyntaxtextarea.*; } + /** + * Java has several context-sensitive keywords that are only keywords in + * very specific constructs. Highlight them conservatively to avoid + * false positives for methods and fields such as "open()" or "record". + */ + private int getContextualTokenType(char[] array, int start, int end, + int tokenType) { + + if (isToken(array, start, end, "yield")) { + return isYieldKeyword(array, start) ? + tokenType : TokenTypes.IDENTIFIER; + } + + if (isToken(array, start, end, "open")) { + return nextTokenEquals(array, end, "module") ? + tokenType : TokenTypes.IDENTIFIER; + } + + if (isToken(array, start, end, "module")) { + return nextTokenStartsIdentifier(array, end) && + (noPreviousToken(start) || + previousTokenEquals(array, start, "open")) ? + tokenType : TokenTypes.IDENTIFIER; + } + + if (isToken(array, start, end, "sealed") || + isToken(array, start, end, "non-sealed")) { + return nextTokenEquals(array, end, "class") || + nextTokenEquals(array, end, "interface") ? + tokenType : TokenTypes.IDENTIFIER; + } + + if (isToken(array, start, end, "exports") || + isToken(array, start, end, "opens") || + isToken(array, start, end, "permits") || + isToken(array, start, end, "provides") || + isToken(array, start, end, "record") || + isToken(array, start, end, "requires") || + isToken(array, start, end, "to") || + isToken(array, start, end, "transitive") || + isToken(array, start, end, "uses") || + isToken(array, start, end, "var") || + isToken(array, start, end, "with")) { + return nextTokenStartsIdentifier(array, end) ? + tokenType : TokenTypes.IDENTIFIER; + } + + return tokenType; + } + + + private int findNextNonWhitespace(char[] array, int pos) { + int end = s.offset + s.count; + while (pos < end && Character.isWhitespace(array[pos])) { + pos++; + } + return pos < end ? pos : -1; + } + + + private int findPreviousNonWhitespace(char[] array, int pos) { + while (pos >= s.offset && Character.isWhitespace(array[pos])) { + pos--; + } + return pos; + } + + + private boolean isToken(char[] array, int start, int end, String lexeme) { + if (end - start + 1 != lexeme.length()) { + return false; + } + for (int i = 0; i < lexeme.length(); i++) { + if (array[start + i] != lexeme.charAt(i)) { + return false; + } + } + return true; + } + + + private boolean isYieldKeyword(char[] array, int start) { + int prev = findPreviousNonWhitespace(array, start - 1); + if (prev < 0) { + return false; + } + if (array[prev] == ':' || array[prev] == '{') { + return true; + } + if (array[prev] == '>') { + int prevPrev = findPreviousNonWhitespace(array, prev - 1); + return prevPrev >= 0 && array[prevPrev] == '-'; + } + return false; + } + + + private boolean nextTokenEquals(char[] array, int end, String lexeme) { + int next = findNextNonWhitespace(array, end + 1); + if (next < 0 || !Character.isJavaIdentifierStart(array[next])) { + return false; + } + int tokenEnd = next + lexeme.length(); + if (tokenEnd > s.offset + s.count) { + return false; + } + for (int i = 0; i < lexeme.length(); i++) { + if (array[next + i] != lexeme.charAt(i)) { + return false; + } + } + return tokenEnd == s.offset + s.count || + !Character.isJavaIdentifierPart(array[tokenEnd]); + } + + + private boolean nextTokenStartsIdentifier(char[] array, int end) { + int next = findNextNonWhitespace(array, end + 1); + return next >= 0 && Character.isJavaIdentifierStart(array[next]); + } + + + private boolean noPreviousToken(int start) { + return findPreviousNonWhitespace(s.array, start - 1) < 0; + } + + + private boolean previousTokenEquals(char[] array, int start, String lexeme) { + int prev = findPreviousNonWhitespace(array, start - 1); + if (prev < 0 || !Character.isJavaIdentifierPart(array[prev])) { + return false; + } + int tokenStart = prev; + while (tokenStart > s.offset && + Character.isJavaIdentifierPart(array[tokenStart - 1])) { + tokenStart--; + } + return isToken(array, tokenStart, prev, lexeme); + } + + /** * Adds the token specified to the current linked list of tokens. * @@ -120,6 +261,11 @@ import org.fife.ui.rsyntaxtextarea.*; @Override public void addToken(char[] array, int start, int end, int tokenType, int startOffset, boolean hyperlink) { + if (tokenType == TokenTypes.DATA_TYPE || + tokenType == TokenTypes.RESERVED_WORD || + tokenType == TokenTypes.RESERVED_WORD_2) { + tokenType = getContextualTokenType(array, start, end, tokenType); + } super.addToken(array, start,end, tokenType, startOffset, hyperlink); zzStartRead = zzMarkedPos; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index f75175d13..3f324165b 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5157,6 +5157,147 @@ public JavaTokenMaker() { } + /** + * Java has several context-sensitive keywords that are only keywords in + * very specific constructs. Highlight them conservatively to avoid + * false positives for methods and fields such as "open()" or "record". + */ + private int getContextualTokenType(char[] array, int start, int end, + int tokenType) { + + if (isToken(array, start, end, "yield")) { + return isYieldKeyword(array, start) ? + tokenType : TokenTypes.IDENTIFIER; + } + + if (isToken(array, start, end, "open")) { + return nextTokenEquals(array, end, "module") ? + tokenType : TokenTypes.IDENTIFIER; + } + + if (isToken(array, start, end, "module")) { + return nextTokenStartsIdentifier(array, end) && + (noPreviousToken(start) || + previousTokenEquals(array, start, "open")) ? + tokenType : TokenTypes.IDENTIFIER; + } + + if (isToken(array, start, end, "sealed") || + isToken(array, start, end, "non-sealed")) { + return nextTokenEquals(array, end, "class") || + nextTokenEquals(array, end, "interface") ? + tokenType : TokenTypes.IDENTIFIER; + } + + if (isToken(array, start, end, "exports") || + isToken(array, start, end, "opens") || + isToken(array, start, end, "permits") || + isToken(array, start, end, "provides") || + isToken(array, start, end, "record") || + isToken(array, start, end, "requires") || + isToken(array, start, end, "to") || + isToken(array, start, end, "transitive") || + isToken(array, start, end, "uses") || + isToken(array, start, end, "var") || + isToken(array, start, end, "with")) { + return nextTokenStartsIdentifier(array, end) ? + tokenType : TokenTypes.IDENTIFIER; + } + + return tokenType; + } + + + private int findNextNonWhitespace(char[] array, int pos) { + int end = s.offset + s.count; + while (pos < end && Character.isWhitespace(array[pos])) { + pos++; + } + return pos < end ? pos : -1; + } + + + private int findPreviousNonWhitespace(char[] array, int pos) { + while (pos >= s.offset && Character.isWhitespace(array[pos])) { + pos--; + } + return pos; + } + + + private boolean isToken(char[] array, int start, int end, String lexeme) { + if (end - start + 1 != lexeme.length()) { + return false; + } + for (int i = 0; i < lexeme.length(); i++) { + if (array[start + i] != lexeme.charAt(i)) { + return false; + } + } + return true; + } + + + private boolean isYieldKeyword(char[] array, int start) { + int prev = findPreviousNonWhitespace(array, start - 1); + if (prev < 0) { + return false; + } + if (array[prev] == ':' || array[prev] == '{') { + return true; + } + if (array[prev] == '>') { + int prevPrev = findPreviousNonWhitespace(array, prev - 1); + return prevPrev >= 0 && array[prevPrev] == '-'; + } + return false; + } + + + private boolean nextTokenEquals(char[] array, int end, String lexeme) { + int next = findNextNonWhitespace(array, end + 1); + if (next < 0 || !Character.isJavaIdentifierStart(array[next])) { + return false; + } + int tokenEnd = next + lexeme.length(); + if (tokenEnd > s.offset + s.count) { + return false; + } + for (int i = 0; i < lexeme.length(); i++) { + if (array[next + i] != lexeme.charAt(i)) { + return false; + } + } + return tokenEnd == s.offset + s.count || + !Character.isJavaIdentifierPart(array[tokenEnd]); + } + + + private boolean nextTokenStartsIdentifier(char[] array, int end) { + int next = findNextNonWhitespace(array, end + 1); + return next >= 0 && Character.isJavaIdentifierStart(array[next]); + } + + + private boolean noPreviousToken(int start) { + return findPreviousNonWhitespace(s.array, start - 1) < 0; + } + + + private boolean previousTokenEquals(char[] array, int start, String lexeme) { + int prev = findPreviousNonWhitespace(array, start - 1); + if (prev < 0 || !Character.isJavaIdentifierPart(array[prev])) { + return false; + } + int tokenStart = prev; + while (tokenStart > s.offset && + Character.isJavaIdentifierPart(array[tokenStart - 1])) { + tokenStart--; + } + return isToken(array, tokenStart, prev, lexeme); + } + + /** * Adds the token specified to the current linked list of tokens. * @@ -5205,6 +5346,11 @@ private void addToken(int start, int end, int tokenType) { @Override public void addToken(char[] array, int start, int end, int tokenType, int startOffset, boolean hyperlink) { + if (tokenType == TokenTypes.DATA_TYPE || + tokenType == TokenTypes.RESERVED_WORD || + tokenType == TokenTypes.RESERVED_WORD_2) { + tokenType = getContextualTokenType(array, start, end, tokenType); + } super.addToken(array, start,end, tokenType, startOffset, hyperlink); zzStartRead = zzMarkedPos; } diff --git a/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java b/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java index f2c333694..d412359f2 100755 --- a/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java +++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java @@ -30,6 +30,27 @@ protected TokenMaker createTokenMaker() { } + private void assertTokenTypeForLexeme(String code, String lexeme, + int expectedType) { + + Token token = createTokenMaker().getTokenList(createSegment(code), + TokenTypes.NULL, 0); + + while (token != null && token.isPaintable()) { + if (lexeme.equals(token.getLexeme())) { + Assertions.assertEquals(expectedType, token.getType(), + "Unexpected token type for token: " + token + + " (code snippet: \"" + code + "\")"); + return; + } + token = token.getNextToken(); + } + + Assertions.fail("Token not found: " + lexeme + " in snippet \"" + + code + "\""); + } + + @Test @Override public void testCommon_GetLineCommentStartAndEnd() { @@ -786,7 +807,6 @@ void testKeywords() { "do", "else", "enum", - "exports", "extends", "final", "finally", @@ -797,22 +817,13 @@ void testKeywords() { "import", "instanceof", "interface", - "module", "native", "new", - "non-sealed", "null", - "open", - "opens", "package", - "permits", "private", "protected", - "provides", "public", - "record", - "requires", - "sealed", "static", "strictfp", "super", @@ -821,15 +832,11 @@ void testKeywords() { "this", "throw", "throws", - "to", "transient", - "transitive", "try", - "uses", "void", "volatile", - "while", - "with" + "while" ); } @@ -837,12 +844,81 @@ void testKeywords() { @Test void testKeywords_exitingMethod() { assertAllTokensOfType(TokenTypes.RESERVED_WORD_2, - "return", - "yield" + "return" ); } + @Test + void testRestrictedKeywords_inJavaConstructs() { + + assertTokenTypeForLexeme("open module demo {", "open", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("open module demo {", "module", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("record Point(int x) {", "record", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("sealed interface Shape permits Circle {", "sealed", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("sealed interface Shape permits Circle {", "permits", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("non-sealed class Square extends Shape {", "non-sealed", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("exports com.example to com.client;", "exports", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("exports com.example to com.client;", "to", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("opens com.example.internal;", "opens", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("requires transitive com.example;", "requires", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("requires transitive com.example;", "transitive", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("uses com.example.Service;", "uses", + TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme( + "provides com.example.Service with com.example.ServiceImpl;", + "provides", TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme( + "provides com.example.Service with com.example.ServiceImpl;", + "with", TokenTypes.RESERVED_WORD); + assertTokenTypeForLexeme("var answer = 42;", "var", + TokenTypes.DATA_TYPE); + assertTokenTypeForLexeme("case 1 -> yield 42;", "yield", + TokenTypes.RESERVED_WORD_2); + } + + + @Test + void testRestrictedKeywords_usedAsIdentifiers() { + + String[] restrictedKeywords = { + "exports", + "module", + "open", + "opens", + "permits", + "provides", + "record", + "requires", + "sealed", + "to", + "transitive", + "uses", + "var", + "with", + "yield" + }; + + for (String keyword : restrictedKeywords) { + assertTokenTypeForLexeme("public int " + keyword + "() {", keyword, + TokenTypes.IDENTIFIER); + assertTokenTypeForLexeme("this." + keyword + "();", keyword, + TokenTypes.IDENTIFIER); + } + } + + @Test void testMultiLineComments() { assertAllTokensOfType(TokenTypes.COMMENT_MULTILINE, From 17c672a01926b81647a1d6d8894a01287ace92ff Mon Sep 17 00:00:00 2001 From: nbauma109 Date: Wed, 15 Apr 2026 13:04:58 +0200 Subject: [PATCH 02/19] fix missing coverage --- .../modes/JavaTokenMakerTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java b/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java index d412359f2..606cb71f1 100755 --- a/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java +++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java @@ -919,6 +919,26 @@ void testRestrictedKeywords_usedAsIdentifiers() { } + @Test + void testRestrictedKeywords_additionalContextCoverage() { + + assertTokenTypeForLexeme("yield;", "yield", + TokenTypes.IDENTIFIER); + assertTokenTypeForLexeme("{ yield 42; }", "yield", + TokenTypes.RESERVED_WORD_2); + assertTokenTypeForLexeme("open mod", "open", + TokenTypes.IDENTIFIER); + assertTokenTypeForLexeme("open mutable", "open", + TokenTypes.IDENTIFIER); + assertTokenTypeForLexeme("open modules", "open", + TokenTypes.IDENTIFIER); + assertTokenTypeForLexeme("foo.module demo {", "module", + TokenTypes.IDENTIFIER); + assertTokenTypeForLexeme("opened module demo {", "module", + TokenTypes.IDENTIFIER); + } + + @Test void testMultiLineComments() { assertAllTokensOfType(TokenTypes.COMMENT_MULTILINE, From ad4de9bb0a6edd9ae9e93e2c3aac3b8d836f75b2 Mon Sep 17 00:00:00 2001 From: nbauma109 Date: Wed, 15 Apr 2026 13:05:20 +0200 Subject: [PATCH 03/19] add demo JavaContextualKeywordsDemo --- .../demo/JavaContextualKeywordsDemo.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 RSyntaxTextAreaDemo/src/main/java/org/fife/ui/rsyntaxtextarea/demo/JavaContextualKeywordsDemo.java diff --git a/RSyntaxTextAreaDemo/src/main/java/org/fife/ui/rsyntaxtextarea/demo/JavaContextualKeywordsDemo.java b/RSyntaxTextAreaDemo/src/main/java/org/fife/ui/rsyntaxtextarea/demo/JavaContextualKeywordsDemo.java new file mode 100644 index 000000000..810f25919 --- /dev/null +++ b/RSyntaxTextAreaDemo/src/main/java/org/fife/ui/rsyntaxtextarea/demo/JavaContextualKeywordsDemo.java @@ -0,0 +1,106 @@ +package org.fife.ui.rsyntaxtextarea.demo; + +import java.awt.BorderLayout; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.WindowConstants; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rtextarea.RTextScrollPane; + +/** + * A focused demo showing Java restricted keywords both in real language + * constructs and as ordinary identifiers such as member names. + */ +public final class JavaContextualKeywordsDemo extends JFrame { + + private static final long serialVersionUID = 1L; + + private static final String TEXT = + "module demo.sample {\n" + + " exports demo.api to demo.client;\n" + + " opens demo.internal;\n" + + " requires transitive demo.dependency;\n" + + " uses demo.spi.Service;\n" + + " provides demo.spi.Service with demo.impl.ServiceImpl;\n" + + "}\n" + + "\n" + + "sealed interface Shape permits Circle, Square {\n" + + "}\n" + + "\n" + + "non-sealed class Square implements Shape {\n" + + "}\n" + + "\n" + + "record Circle(int radius) implements Shape {\n" + + "\n" + + " private int open;\n" + + " private int yield;\n" + + " private int record;\n" + + "\n" + + " int open() {\n" + + " var value = radius;\n" + + " return value;\n" + + " }\n" + + "\n" + + " int record() {\n" + + " return this.record;\n" + + " }\n" + + "\n" + + " int yield() {\n" + + " return this.yield;\n" + + " }\n" + + "}\n" + + "\n" + + "class KeywordUsage {\n" + + "\n" + + " void demo(int record, int open, int yield) {\n" + + " Circle circle = new Circle(4);\n" + + " int value = switch (record) {\n" + + " case 0 -> yield;\n" + + " default -> {\n" + + " yield circle.open() + circle.record() + circle.yield() + open;\n" + + " }\n" + + " };\n" + + " System.out.println(value);\n" + + " }\n" + + "}\n"; + + + private JavaContextualKeywordsDemo() { + + super("Java Contextual Keywords Demo"); + + RSyntaxTextArea textArea = new RSyntaxTextArea(30, 90); + textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); + textArea.setCodeFoldingEnabled(true); + textArea.setAntiAliasingEnabled(true); + textArea.setMarkOccurrences(true); + textArea.setText(TEXT); + int caret = TEXT.indexOf(" int open()"); + textArea.setCaretPosition(caret > -1 ? caret : 0); + + RTextScrollPane scrollPane = new RTextScrollPane(textArea); + setLayout(new BorderLayout()); + add(scrollPane); + + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + pack(); + setLocationRelativeTo(null); + + } + + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception ignored) { + // Ignore and fall back to the default look and feel. + } + new JavaContextualKeywordsDemo().setVisible(true); + }); + } + +} From 70bb1a1e92fff1976d383b4ca342ba976ce3ca8c Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 07:06:46 +0000 Subject: [PATCH 04/19] Address review comments: fix yield/module contextual keywords - Fix noPreviousToken() to use passed array parameter consistently - Remove yield keyword detection after -> (fixes 'case 0 -> yield;') - Simplify yield detection to only treat as keyword after ':' or '{' - Add isModuleKeyword() helper supporting annotations (e.g., @Deprecated module) - Update test case from invalid 'case 1 -> yield 42;' to valid 'case 1 -> { yield 42; }' - Add test for 'case 0 -> yield;' expecting identifier Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/6d7003d5-ef8d-4189-b946-b81cd0e5c2cd Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 31 ++++++++++++------- .../rsyntaxtextarea/modes/JavaTokenMaker.java | 31 ++++++++++++------- .../modes/JavaTokenMakerTest.java | 6 +++- 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 1c581813a..502e72ea9 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -92,8 +92,7 @@ import org.fife.ui.rsyntaxtextarea.*; if (isToken(array, start, end, "module")) { return nextTokenStartsIdentifier(array, end) && - (noPreviousToken(start) || - previousTokenEquals(array, start, "open")) ? + isModuleKeyword(array, start) ? tokenType : TokenTypes.IDENTIFIER; } @@ -155,17 +154,27 @@ import org.fife.ui.rsyntaxtextarea.*; private boolean isYieldKeyword(char[] array, int start) { int prev = findPreviousNonWhitespace(array, start - 1); - if (prev < 0) { - return false; + // yield is a keyword after ':' or '{' (statement context) + if (prev >= s.offset && (array[prev] == ':' || array[prev] == '{')) { + return true; } - if (array[prev] == ':' || array[prev] == '{') { + return false; + } + + + private boolean isModuleKeyword(char[] array, int start) { + // module is a keyword when: + // 1. It's the first token on the line, OR + // 2. It's preceded by "open", OR + // 3. It's preceded by ')' (end of annotation like @Deprecated) + if (noPreviousToken(array, start)) { return true; } - if (array[prev] == '>') { - int prevPrev = findPreviousNonWhitespace(array, prev - 1); - return prevPrev >= 0 && array[prevPrev] == '-'; + if (previousTokenEquals(array, start, "open")) { + return true; } - return false; + int prev = findPreviousNonWhitespace(array, start - 1); + return prev >= s.offset && array[prev] == ')'; } @@ -194,8 +203,8 @@ import org.fife.ui.rsyntaxtextarea.*; } - private boolean noPreviousToken(int start) { - return findPreviousNonWhitespace(s.array, start - 1) < 0; + private boolean noPreviousToken(char[] array, int start) { + return findPreviousNonWhitespace(array, start - 1) < 0; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 3f324165b..1acceb9a9 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5177,8 +5177,7 @@ private int getContextualTokenType(char[] array, int start, int end, if (isToken(array, start, end, "module")) { return nextTokenStartsIdentifier(array, end) && - (noPreviousToken(start) || - previousTokenEquals(array, start, "open")) ? + isModuleKeyword(array, start) ? tokenType : TokenTypes.IDENTIFIER; } @@ -5240,17 +5239,27 @@ private boolean isToken(char[] array, int start, int end, String lexeme) { private boolean isYieldKeyword(char[] array, int start) { int prev = findPreviousNonWhitespace(array, start - 1); - if (prev < 0) { - return false; + // yield is a keyword after ':' or '{' (statement context) + if (prev >= s.offset && (array[prev] == ':' || array[prev] == '{')) { + return true; } - if (array[prev] == ':' || array[prev] == '{') { + return false; + } + + + private boolean isModuleKeyword(char[] array, int start) { + // module is a keyword when: + // 1. It's the first token on the line, OR + // 2. It's preceded by "open", OR + // 3. It's preceded by ')' (end of annotation like @Deprecated) + if (noPreviousToken(array, start)) { return true; } - if (array[prev] == '>') { - int prevPrev = findPreviousNonWhitespace(array, prev - 1); - return prevPrev >= 0 && array[prevPrev] == '-'; + if (previousTokenEquals(array, start, "open")) { + return true; } - return false; + int prev = findPreviousNonWhitespace(array, start - 1); + return prev >= s.offset && array[prev] == ')'; } @@ -5279,8 +5288,8 @@ private boolean nextTokenStartsIdentifier(char[] array, int end) { } - private boolean noPreviousToken(int start) { - return findPreviousNonWhitespace(s.array, start - 1) < 0; + private boolean noPreviousToken(char[] array, int start) { + return findPreviousNonWhitespace(array, start - 1) < 0; } diff --git a/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java b/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java index 606cb71f1..6645455bf 100755 --- a/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java +++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMakerTest.java @@ -884,8 +884,12 @@ void testRestrictedKeywords_inJavaConstructs() { "with", TokenTypes.RESERVED_WORD); assertTokenTypeForLexeme("var answer = 42;", "var", TokenTypes.DATA_TYPE); - assertTokenTypeForLexeme("case 1 -> yield 42;", "yield", + // yield in a block is a keyword + assertTokenTypeForLexeme("case 1 -> { yield 42; }", "yield", TokenTypes.RESERVED_WORD_2); + // yield after -> without block is an identifier/expression + assertTokenTypeForLexeme("case 0 -> yield;", "yield", + TokenTypes.IDENTIFIER); } From 1349b5686b9553eeac9880555876c716a64b3ccb Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 07:38:29 +0000 Subject: [PATCH 05/19] Enhance yield keyword detection for line-start cases Add isYieldKeywordAtLineStart() helper to handle yield statements at the beginning of lines (common formatting in switch blocks). The heuristic checks what follows yield: - If followed by an expression/value (not '(', ':', or ';'), treat as keyword - If followed by ';' or nothing, treat as identifier (ambiguous case) - This enables correct highlighting of 'yield value;' on its own line in switch blocks Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/ff04e4e0-288e-4aeb-b548-64038033ff91 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 17 ++++++++++++++++- .../rsyntaxtextarea/modes/JavaTokenMaker.java | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 502e72ea9..d6482f374 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -152,10 +152,25 @@ import org.fife.ui.rsyntaxtextarea.*; } + private boolean isYieldKeywordAtLineStart(char[] array, int start) { + int next = findNextNonWhitespace(array, start + "yield".length()); + if (next < 0) { + // No token after yield - ambiguous, lean toward identifier + return false; + } + char ch = array[next]; + // yield is a keyword if followed by a value/expression, not if followed by ( or : or ; + return ch != '(' && ch != ':' && ch != ';'; + } + + private boolean isYieldKeyword(char[] array, int start) { int prev = findPreviousNonWhitespace(array, start - 1); + if (prev < s.offset) { + return isYieldKeywordAtLineStart(array, start); + } // yield is a keyword after ':' or '{' (statement context) - if (prev >= s.offset && (array[prev] == ':' || array[prev] == '{')) { + if (array[prev] == ':' || array[prev] == '{') { return true; } return false; diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 1acceb9a9..cb14a54c7 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5237,10 +5237,25 @@ private boolean isToken(char[] array, int start, int end, String lexeme) { } + private boolean isYieldKeywordAtLineStart(char[] array, int start) { + int next = findNextNonWhitespace(array, start + "yield".length()); + if (next < 0) { + // No token after yield - ambiguous, lean toward identifier + return false; + } + char ch = array[next]; + // yield is a keyword if followed by a value/expression, not if followed by ( or : or ; + return ch != '(' && ch != ':' && ch != ';'; + } + + private boolean isYieldKeyword(char[] array, int start) { int prev = findPreviousNonWhitespace(array, start - 1); + if (prev < s.offset) { + return isYieldKeywordAtLineStart(array, start); + } // yield is a keyword after ':' or '{' (statement context) - if (prev >= s.offset && (array[prev] == ':' || array[prev] == '{')) { + if (array[prev] == ':' || array[prev] == '{') { return true; } return false; From 5ed57da6958fc7150c88aebabe04685dab80188e Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 08:21:39 +0000 Subject: [PATCH 06/19] Fix segment offset handling and yield statement detection - Fix noPreviousToken() to check against s.offset instead of 0 (handles non-zero segment offsets on lines after first in documents) - Enhance isYieldKeyword() to recognize yield after ')' and ';' (fixes nested control flow like 'if (b) yield 1;') - Apply fixes to both .java and .flex files for consistency Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/8916a861-fc85-40a9-8db6-ec748a717793 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex | 7 ++++--- .../org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index d6482f374..991681252 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -169,8 +169,9 @@ import org.fife.ui.rsyntaxtextarea.*; if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword after ':' or '{' (statement context) - if (array[prev] == ':' || array[prev] == '{') { + // yield is a keyword in statement contexts: after ':', '{', ')', or ';' + char prevChar = array[prev]; + if (prevChar == ':' || prevChar == '{' || prevChar == ')' || prevChar == ';') { return true; } return false; @@ -219,7 +220,7 @@ import org.fife.ui.rsyntaxtextarea.*; private boolean noPreviousToken(char[] array, int start) { - return findPreviousNonWhitespace(array, start - 1) < 0; + return findPreviousNonWhitespace(array, start - 1) < s.offset; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index cb14a54c7..d1d979db9 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5254,8 +5254,9 @@ private boolean isYieldKeyword(char[] array, int start) { if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword after ':' or '{' (statement context) - if (array[prev] == ':' || array[prev] == '{') { + // yield is a keyword in statement contexts: after ':', '{', ')', or ';' + char prevChar = array[prev]; + if (prevChar == ':' || prevChar == '{' || prevChar == ')' || prevChar == ';') { return true; } return false; @@ -5304,7 +5305,7 @@ private boolean nextTokenStartsIdentifier(char[] array, int end) { private boolean noPreviousToken(char[] array, int start) { - return findPreviousNonWhitespace(array, start - 1) < 0; + return findPreviousNonWhitespace(array, start - 1) < s.offset; } From 2b569c2b27dab5211c66796c700f5d8f6e800a72 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 08:48:53 +0000 Subject: [PATCH 07/19] Address Codex review: enhance contextual keyword detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add findNextNonWhitespaceSkipComments() to skip comments when peeking - Enhance isModuleKeyword() to recognize module after bare annotations (e.g., @Deprecated module) - Add nextClassOrInterfaceKeyword() to accept non-adjacent modifiers after sealed - Update sealed/non-sealed detection to handle cases like "sealed abstract class" - Apply all changes to both JavaTokenMaker.java and .flex files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/d2a92c03-1bdc-41f3-a342-a39dcd06397c Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 102 ++++++++++++++++- .../rsyntaxtextarea/modes/JavaTokenMaker.java | 103 +++++++++++++++++- 2 files changed, 197 insertions(+), 8 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 991681252..ebb01e161 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -98,8 +98,7 @@ import org.fife.ui.rsyntaxtextarea.*; if (isToken(array, start, end, "sealed") || isToken(array, start, end, "non-sealed")) { - return nextTokenEquals(array, end, "class") || - nextTokenEquals(array, end, "interface") ? + return nextClassOrInterfaceKeyword(array, end) ? tokenType : TokenTypes.IDENTIFIER; } @@ -131,6 +130,36 @@ import org.fife.ui.rsyntaxtextarea.*; } + private int findNextNonWhitespaceSkipComments(char[] array, int pos) { + int end = s.offset + s.count; + while (pos < end) { + if (Character.isWhitespace(array[pos])) { + pos++; + continue; + } + // Skip single-line comments + if (pos + 1 < end && array[pos] == '/' && array[pos + 1] == '/') { + // Skip to end of line (which is end of segment in this tokenizer) + return -1; + } + // Skip multi-line comments + if (pos + 1 < end && array[pos] == '/' && array[pos + 1] == '*') { + pos += 2; + while (pos + 1 < end) { + if (array[pos] == '*' && array[pos + 1] == '/') { + pos += 2; + break; + } + pos++; + } + continue; + } + return pos; + } + return -1; + } + + private int findPreviousNonWhitespace(char[] array, int pos) { while (pos >= s.offset && Character.isWhitespace(array[pos])) { pos--; @@ -182,7 +211,8 @@ import org.fife.ui.rsyntaxtextarea.*; // module is a keyword when: // 1. It's the first token on the line, OR // 2. It's preceded by "open", OR - // 3. It's preceded by ')' (end of annotation like @Deprecated) + // 3. It's preceded by ')' (end of annotation like @Deprecated), OR + // 4. It's preceded by '@' identifier (bare annotation like @Deprecated) if (noPreviousToken(array, start)) { return true; } @@ -190,7 +220,71 @@ import org.fife.ui.rsyntaxtextarea.*; return true; } int prev = findPreviousNonWhitespace(array, start - 1); - return prev >= s.offset && array[prev] == ')'; + if (prev >= s.offset) { + // Check for closing paren of annotation + if (array[prev] == ')') { + return true; + } + // Check for bare annotation (e.g., @Deprecated module) + if (Character.isJavaIdentifierPart(array[prev])) { + int tokenStart = prev; + while (tokenStart > s.offset && Character.isJavaIdentifierPart(array[tokenStart - 1])) { + tokenStart--; + } + // Check if there's an @ before this identifier + if (tokenStart > s.offset) { + int beforeId = findPreviousNonWhitespace(array, tokenStart - 1); + if (beforeId >= s.offset && array[beforeId] == '@') { + return true; + } + } + } + } + return false; + } + + + /** + * Checks if the next keyword after the given position is "class" or "interface", + * skipping over modifiers (abstract, final, static, etc.) and comments. + * This is used for sealed/non-sealed keyword detection. + */ + private boolean nextClassOrInterfaceKeyword(char[] array, int end) { + int pos = findNextNonWhitespaceSkipComments(array, end + 1); + while (pos >= 0) { + if (!Character.isJavaIdentifierStart(array[pos])) { + return false; + } + // Find the end of this identifier + int tokenStart = pos; + while (pos < s.offset + s.count && Character.isJavaIdentifierPart(array[pos])) { + pos++; + } + int tokenEnd = pos - 1; + + // Check if it's "class" or "interface" + if (isToken(array, tokenStart, tokenEnd, "class") || + isToken(array, tokenStart, tokenEnd, "interface")) { + return true; + } + + // Check if it's a modifier we should skip + if (isToken(array, tokenStart, tokenEnd, "abstract") || + isToken(array, tokenStart, tokenEnd, "final") || + isToken(array, tokenStart, tokenEnd, "static") || + isToken(array, tokenStart, tokenEnd, "public") || + isToken(array, tokenStart, tokenEnd, "protected") || + isToken(array, tokenStart, tokenEnd, "private") || + isToken(array, tokenStart, tokenEnd, "strictfp")) { + // Skip this modifier and continue looking + pos = findNextNonWhitespaceSkipComments(array, pos); + continue; + } + + // Found something that's not a modifier or class/interface + return false; + } + return false; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index d1d979db9..71e11a572 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5183,8 +5183,7 @@ private int getContextualTokenType(char[] array, int start, int end, if (isToken(array, start, end, "sealed") || isToken(array, start, end, "non-sealed")) { - return nextTokenEquals(array, end, "class") || - nextTokenEquals(array, end, "interface") ? + return nextClassOrInterfaceKeyword(array, end) ? tokenType : TokenTypes.IDENTIFIER; } @@ -5216,6 +5215,36 @@ private int findNextNonWhitespace(char[] array, int pos) { } + private int findNextNonWhitespaceSkipComments(char[] array, int pos) { + int end = s.offset + s.count; + while (pos < end) { + if (Character.isWhitespace(array[pos])) { + pos++; + continue; + } + // Skip single-line comments + if (pos + 1 < end && array[pos] == '/' && array[pos + 1] == '/') { + // Skip to end of line (which is end of segment in this tokenizer) + return -1; + } + // Skip multi-line comments + if (pos + 1 < end && array[pos] == '/' && array[pos + 1] == '*') { + pos += 2; + while (pos + 1 < end) { + if (array[pos] == '*' && array[pos + 1] == '/') { + pos += 2; + break; + } + pos++; + } + continue; + } + return pos; + } + return -1; + } + + private int findPreviousNonWhitespace(char[] array, int pos) { while (pos >= s.offset && Character.isWhitespace(array[pos])) { pos--; @@ -5267,7 +5296,8 @@ private boolean isModuleKeyword(char[] array, int start) { // module is a keyword when: // 1. It's the first token on the line, OR // 2. It's preceded by "open", OR - // 3. It's preceded by ')' (end of annotation like @Deprecated) + // 3. It's preceded by ')' (end of annotation like @Deprecated), OR + // 4. It's preceded by '@' identifier (bare annotation like @Deprecated) if (noPreviousToken(array, start)) { return true; } @@ -5275,7 +5305,72 @@ private boolean isModuleKeyword(char[] array, int start) { return true; } int prev = findPreviousNonWhitespace(array, start - 1); - return prev >= s.offset && array[prev] == ')'; + if (prev >= s.offset) { + // Check for closing paren of annotation + if (array[prev] == ')') { + return true; + } + // Check for bare annotation (e.g., @Deprecated module) + // Look for identifier preceded by @ + if (Character.isJavaIdentifierPart(array[prev])) { + int tokenStart = prev; + while (tokenStart > s.offset && Character.isJavaIdentifierPart(array[tokenStart - 1])) { + tokenStart--; + } + // Check if there's an @ before this identifier + if (tokenStart > s.offset) { + int beforeId = findPreviousNonWhitespace(array, tokenStart - 1); + if (beforeId >= s.offset && array[beforeId] == '@') { + return true; + } + } + } + } + return false; + } + + + /** + * Checks if the next keyword after the given position is "class" or "interface", + * skipping over modifiers (abstract, final, static, etc.) and comments. + * This is used for sealed/non-sealed keyword detection. + */ + private boolean nextClassOrInterfaceKeyword(char[] array, int end) { + int pos = findNextNonWhitespaceSkipComments(array, end + 1); + while (pos >= 0) { + if (!Character.isJavaIdentifierStart(array[pos])) { + return false; + } + // Find the end of this identifier + int tokenStart = pos; + while (pos < s.offset + s.count && Character.isJavaIdentifierPart(array[pos])) { + pos++; + } + int tokenEnd = pos - 1; + + // Check if it's "class" or "interface" + if (isToken(array, tokenStart, tokenEnd, "class") || + isToken(array, tokenStart, tokenEnd, "interface")) { + return true; + } + + // Check if it's a modifier we should skip + if (isToken(array, tokenStart, tokenEnd, "abstract") || + isToken(array, tokenStart, tokenEnd, "final") || + isToken(array, tokenStart, tokenEnd, "static") || + isToken(array, tokenStart, tokenEnd, "public") || + isToken(array, tokenStart, tokenEnd, "protected") || + isToken(array, tokenStart, tokenEnd, "private") || + isToken(array, tokenStart, tokenEnd, "strictfp")) { + // Skip this modifier and continue looking + pos = findNextNonWhitespaceSkipComments(array, pos); + continue; + } + + // Found something that's not a modifier or class/interface + return false; + } + return false; } From fd39d3e09a2c3e9da4110219cc50c202039c330c Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:09:16 +0000 Subject: [PATCH 08/19] Fix contextual keyword lookahead to skip comments and handle yield (expr) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update isYieldKeywordAtLineStart to use findNextNonWhitespaceSkipComments - Fix isYieldKeywordAtLineStart to treat 'yield (' as keyword (yield statement) - Update nextTokenEquals to skip comments when peeking next token - Update nextTokenStartsIdentifier to skip comments when peeking next token - Apply all changes to both JavaTokenMaker.java and .flex files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/a1b21b5b-8e51-472e-abe0-eeb48a953fe0 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex | 11 ++++++----- .../fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index ebb01e161..46d3758ef 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -182,14 +182,15 @@ import org.fife.ui.rsyntaxtextarea.*; private boolean isYieldKeywordAtLineStart(char[] array, int start) { - int next = findNextNonWhitespace(array, start + "yield".length()); + int next = findNextNonWhitespaceSkipComments(array, start + "yield".length()); if (next < 0) { // No token after yield - ambiguous, lean toward identifier return false; } char ch = array[next]; - // yield is a keyword if followed by a value/expression, not if followed by ( or : or ; - return ch != '(' && ch != ':' && ch != ';'; + // yield is a keyword if followed by a value/expression (including '(' for yield (expr)) + // not if followed by ':' or ';' (those would make it an identifier/label) + return ch != ':' && ch != ';'; } @@ -289,7 +290,7 @@ import org.fife.ui.rsyntaxtextarea.*; private boolean nextTokenEquals(char[] array, int end, String lexeme) { - int next = findNextNonWhitespace(array, end + 1); + int next = findNextNonWhitespaceSkipComments(array, end + 1); if (next < 0 || !Character.isJavaIdentifierStart(array[next])) { return false; } @@ -308,7 +309,7 @@ import org.fife.ui.rsyntaxtextarea.*; private boolean nextTokenStartsIdentifier(char[] array, int end) { - int next = findNextNonWhitespace(array, end + 1); + int next = findNextNonWhitespaceSkipComments(array, end + 1); return next >= 0 && Character.isJavaIdentifierStart(array[next]); } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 71e11a572..9e1e071c4 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5267,14 +5267,15 @@ private boolean isToken(char[] array, int start, int end, String lexeme) { private boolean isYieldKeywordAtLineStart(char[] array, int start) { - int next = findNextNonWhitespace(array, start + "yield".length()); + int next = findNextNonWhitespaceSkipComments(array, start + "yield".length()); if (next < 0) { // No token after yield - ambiguous, lean toward identifier return false; } char ch = array[next]; - // yield is a keyword if followed by a value/expression, not if followed by ( or : or ; - return ch != '(' && ch != ':' && ch != ';'; + // yield is a keyword if followed by a value/expression (including '(' for yield (expr)) + // not if followed by ':' or ';' (those would make it an identifier/label) + return ch != ':' && ch != ';'; } @@ -5375,7 +5376,7 @@ private boolean nextClassOrInterfaceKeyword(char[] array, int end) { private boolean nextTokenEquals(char[] array, int end, String lexeme) { - int next = findNextNonWhitespace(array, end + 1); + int next = findNextNonWhitespaceSkipComments(array, end + 1); if (next < 0 || !Character.isJavaIdentifierStart(array[next])) { return false; } @@ -5394,7 +5395,7 @@ private boolean nextTokenEquals(char[] array, int end, String lexeme) { private boolean nextTokenStartsIdentifier(char[] array, int end) { - int next = findNextNonWhitespace(array, end + 1); + int next = findNextNonWhitespaceSkipComments(array, end + 1); return next >= 0 && Character.isJavaIdentifierStart(array[next]); } From 78269c3daca648d407e604137781d4a1522d7a1d Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:22:35 +0000 Subject: [PATCH 09/19] Refine contextual keyword detection for yield and sealed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove ')' from yield keyword detection to avoid treating yield as keyword after any closing paren (more conservative approach to prevent false positives) - Add annotation skipping in nextClassOrInterfaceKeyword to handle cases like "sealed @Deprecated class" - Apply changes to both JavaTokenMaker.java and .flex files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/d609853f-783c-4e7c-b4fe-eef4b11479e3 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 37 +++++++++++++++++-- .../rsyntaxtextarea/modes/JavaTokenMaker.java | 37 +++++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 46d3758ef..8203a8fda 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -199,9 +199,9 @@ import org.fife.ui.rsyntaxtextarea.*; if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword in statement contexts: after ':', '{', ')', or ';' + // yield is a keyword in statement contexts: after ':', '{', or ';' char prevChar = array[prev]; - if (prevChar == ':' || prevChar == '{' || prevChar == ')' || prevChar == ';') { + if (prevChar == ':' || prevChar == '{' || prevChar == ';') { return true; } return false; @@ -247,12 +247,43 @@ import org.fife.ui.rsyntaxtextarea.*; /** * Checks if the next keyword after the given position is "class" or "interface", - * skipping over modifiers (abstract, final, static, etc.) and comments. + * skipping over modifiers (abstract, final, static, etc.), annotations, and comments. * This is used for sealed/non-sealed keyword detection. */ private boolean nextClassOrInterfaceKeyword(char[] array, int end) { int pos = findNextNonWhitespaceSkipComments(array, end + 1); while (pos >= 0) { + // Skip annotations (@...) + if (array[pos] == '@') { + // Skip the @ symbol + pos = findNextNonWhitespaceSkipComments(array, pos + 1); + if (pos < 0 || !Character.isJavaIdentifierStart(array[pos])) { + return false; + } + // Skip the annotation name + while (pos < s.offset + s.count && Character.isJavaIdentifierPart(array[pos])) { + pos++; + } + // Skip annotation parameters if present + pos = findNextNonWhitespaceSkipComments(array, pos); + if (pos >= 0 && array[pos] == '(') { + // Skip to closing paren + int depth = 1; + pos++; + while (pos < s.offset + s.count && depth > 0) { + if (array[pos] == '(') { + depth++; + } else if (array[pos] == ')') { + depth--; + } + pos++; + } + } + // Continue to next token after annotation + pos = findNextNonWhitespaceSkipComments(array, pos); + continue; + } + if (!Character.isJavaIdentifierStart(array[pos])) { return false; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 9e1e071c4..bc1239795 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5284,9 +5284,9 @@ private boolean isYieldKeyword(char[] array, int start) { if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword in statement contexts: after ':', '{', ')', or ';' + // yield is a keyword in statement contexts: after ':', '{', or ';' char prevChar = array[prev]; - if (prevChar == ':' || prevChar == '{' || prevChar == ')' || prevChar == ';') { + if (prevChar == ':' || prevChar == '{' || prevChar == ';') { return true; } return false; @@ -5333,12 +5333,43 @@ private boolean isModuleKeyword(char[] array, int start) { /** * Checks if the next keyword after the given position is "class" or "interface", - * skipping over modifiers (abstract, final, static, etc.) and comments. + * skipping over modifiers (abstract, final, static, etc.), annotations, and comments. * This is used for sealed/non-sealed keyword detection. */ private boolean nextClassOrInterfaceKeyword(char[] array, int end) { int pos = findNextNonWhitespaceSkipComments(array, end + 1); while (pos >= 0) { + // Skip annotations (@...) + if (array[pos] == '@') { + // Skip the @ symbol + pos = findNextNonWhitespaceSkipComments(array, pos + 1); + if (pos < 0 || !Character.isJavaIdentifierStart(array[pos])) { + return false; + } + // Skip the annotation name + while (pos < s.offset + s.count && Character.isJavaIdentifierPart(array[pos])) { + pos++; + } + // Skip annotation parameters if present + pos = findNextNonWhitespaceSkipComments(array, pos); + if (pos >= 0 && array[pos] == '(') { + // Skip to closing paren + int depth = 1; + pos++; + while (pos < s.offset + s.count && depth > 0) { + if (array[pos] == '(') { + depth++; + } else if (array[pos] == ')') { + depth--; + } + pos++; + } + } + // Continue to next token after annotation + pos = findNextNonWhitespaceSkipComments(array, pos); + continue; + } + if (!Character.isJavaIdentifierStart(array[pos])) { return false; } From 8dfb2593869df3372582fc45b7d5a62750b461c6 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:43:02 +0000 Subject: [PATCH 10/19] Improve contextual keyword detection across line breaks and comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add findPreviousNonWhitespaceSkipComments to skip comments when checking prefix context - Update noPreviousToken to use comment-skipping version for module detection - Improve restricted keyword detection (exports, record, etc.) to handle line breaks while still recognizing method/field usage - Re-add ')' to yield keyword detection for conditional expressions (if (cond) yield value;) - Apply all changes to both JavaTokenMaker.java and .flex files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/25ff1bd0-82fe-4c7b-990a-055758419dca Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 48 +++++++++++++++++-- .../rsyntaxtextarea/modes/JavaTokenMaker.java | 48 +++++++++++++++++-- 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 8203a8fda..5e490f08e 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -113,8 +113,17 @@ import org.fife.ui.rsyntaxtextarea.*; isToken(array, start, end, "uses") || isToken(array, start, end, "var") || isToken(array, start, end, "with")) { - return nextTokenStartsIdentifier(array, end) ? - tokenType : TokenTypes.IDENTIFIER; + // Check if used as method/field name (preceded by '.' or followed by '(') + int prev = findPreviousNonWhitespace(array, start - 1); + if (prev >= s.offset && array[prev] == '.') { + return TokenTypes.IDENTIFIER; + } + int next = findNextNonWhitespaceSkipComments(array, end + 1); + if (next >= 0 && array[next] == '(') { + return TokenTypes.IDENTIFIER; + } + // Otherwise treat as keyword (conservative for module declarations, etc.) + return tokenType; } return tokenType; @@ -160,6 +169,34 @@ import org.fife.ui.rsyntaxtextarea.*; } + private int findPreviousNonWhitespaceSkipComments(char[] array, int pos) { + while (pos >= s.offset) { + if (Character.isWhitespace(array[pos])) { + pos--; + continue; + } + // Skip multi-line comments (work backwards from */) + if (pos > s.offset && array[pos] == '/' && array[pos - 1] == '*') { + // Found end of comment, skip backwards to find start + pos -= 2; + while (pos > s.offset) { + if (array[pos] == '*' && array[pos - 1] == '/') { + pos -= 2; + break; + } + pos--; + } + if (pos < s.offset) { + return -1; + } + continue; + } + return pos; + } + return pos; + } + + private int findPreviousNonWhitespace(char[] array, int pos) { while (pos >= s.offset && Character.isWhitespace(array[pos])) { pos--; @@ -199,9 +236,10 @@ import org.fife.ui.rsyntaxtextarea.*; if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword in statement contexts: after ':', '{', or ';' + // yield is a keyword in statement contexts: after ':', '{', ';', or ')' + // The ')' case handles control flow like "if (cond) yield value;" char prevChar = array[prev]; - if (prevChar == ':' || prevChar == '{' || prevChar == ';') { + if (prevChar == ':' || prevChar == '{' || prevChar == ';' || prevChar == ')') { return true; } return false; @@ -346,7 +384,7 @@ import org.fife.ui.rsyntaxtextarea.*; private boolean noPreviousToken(char[] array, int start) { - return findPreviousNonWhitespace(array, start - 1) < s.offset; + return findPreviousNonWhitespaceSkipComments(array, start - 1) < s.offset; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index bc1239795..7d4fb068f 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5198,8 +5198,17 @@ private int getContextualTokenType(char[] array, int start, int end, isToken(array, start, end, "uses") || isToken(array, start, end, "var") || isToken(array, start, end, "with")) { - return nextTokenStartsIdentifier(array, end) ? - tokenType : TokenTypes.IDENTIFIER; + // Check if used as method/field name (preceded by '.' or followed by '(') + int prev = findPreviousNonWhitespace(array, start - 1); + if (prev >= s.offset && array[prev] == '.') { + return TokenTypes.IDENTIFIER; + } + int next = findNextNonWhitespaceSkipComments(array, end + 1); + if (next >= 0 && array[next] == '(') { + return TokenTypes.IDENTIFIER; + } + // Otherwise treat as keyword (conservative for module declarations, etc.) + return tokenType; } return tokenType; @@ -5245,6 +5254,34 @@ private int findNextNonWhitespaceSkipComments(char[] array, int pos) { } + private int findPreviousNonWhitespaceSkipComments(char[] array, int pos) { + while (pos >= s.offset) { + if (Character.isWhitespace(array[pos])) { + pos--; + continue; + } + // Skip multi-line comments (work backwards from */) + if (pos > s.offset && array[pos] == '/' && array[pos - 1] == '*') { + // Found end of comment, skip backwards to find start + pos -= 2; + while (pos > s.offset) { + if (array[pos] == '*' && array[pos - 1] == '/') { + pos -= 2; + break; + } + pos--; + } + if (pos < s.offset) { + return -1; + } + continue; + } + return pos; + } + return pos; + } + + private int findPreviousNonWhitespace(char[] array, int pos) { while (pos >= s.offset && Character.isWhitespace(array[pos])) { pos--; @@ -5284,9 +5321,10 @@ private boolean isYieldKeyword(char[] array, int start) { if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword in statement contexts: after ':', '{', or ';' + // yield is a keyword in statement contexts: after ':', '{', ';', or ')' + // The ')' case handles control flow like "if (cond) yield value;" char prevChar = array[prev]; - if (prevChar == ':' || prevChar == '{' || prevChar == ';') { + if (prevChar == ':' || prevChar == '{' || prevChar == ';' || prevChar == ')') { return true; } return false; @@ -5432,7 +5470,7 @@ private boolean nextTokenStartsIdentifier(char[] array, int end) { private boolean noPreviousToken(char[] array, int start) { - return findPreviousNonWhitespace(array, start - 1) < s.offset; + return findPreviousNonWhitespaceSkipComments(array, start - 1) < s.offset; } From 931529d0aeeffb679c4b271b3924ba9021e31e26 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 10:01:58 +0000 Subject: [PATCH 11/19] Improve contextual keyword detection across line breaks and comments Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/43702e97-c42d-4fdb-90c4-c021c615ac6b Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 22 ++++++++++++++---- .../rsyntaxtextarea/modes/JavaTokenMaker.java | 23 ++++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 5e490f08e..1f4de84ce 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -86,6 +86,13 @@ import org.fife.ui.rsyntaxtextarea.*; } if (isToken(array, start, end, "open")) { + // Be conservative: treat as keyword if we can't verify context (e.g., next line) + // This handles "open\nmodule" formatting + int next = findNextNonWhitespaceSkipComments(array, end + 1); + if (next < 0) { + // Next token not on this line - conservatively treat as keyword + return tokenType; + } return nextTokenEquals(array, end, "module") ? tokenType : TokenTypes.IDENTIFIER; } @@ -264,13 +271,14 @@ import org.fife.ui.rsyntaxtextarea.*; if (array[prev] == ')') { return true; } - // Check for bare annotation (e.g., @Deprecated module) + // Check for bare annotation (e.g., @Deprecated module) or qualified (e.g., @com.acme.Visible module) + // Look for identifier or qualified name preceded by @ if (Character.isJavaIdentifierPart(array[prev])) { int tokenStart = prev; - while (tokenStart > s.offset && Character.isJavaIdentifierPart(array[tokenStart - 1])) { + while (tokenStart > s.offset && (Character.isJavaIdentifierPart(array[tokenStart - 1]) || array[tokenStart - 1] == '.')) { tokenStart--; } - // Check if there's an @ before this identifier + // Check if there's an @ before this identifier/qualified name if (tokenStart > s.offset) { int beforeId = findPreviousNonWhitespace(array, tokenStart - 1); if (beforeId >= s.offset && array[beforeId] == '@') { @@ -290,6 +298,10 @@ import org.fife.ui.rsyntaxtextarea.*; */ private boolean nextClassOrInterfaceKeyword(char[] array, int end) { int pos = findNextNonWhitespaceSkipComments(array, end + 1); + // Conservative: if next token not on current line, treat as keyword to handle line breaks + if (pos < 0) { + return true; + } while (pos >= 0) { // Skip annotations (@...) if (array[pos] == '@') { @@ -298,8 +310,8 @@ import org.fife.ui.rsyntaxtextarea.*; if (pos < 0 || !Character.isJavaIdentifierStart(array[pos])) { return false; } - // Skip the annotation name - while (pos < s.offset + s.count && Character.isJavaIdentifierPart(array[pos])) { + // Skip the annotation name (including qualified names like @com.acme.Anno) + while (pos < s.offset + s.count && (Character.isJavaIdentifierPart(array[pos]) || array[pos] == '.')) { pos++; } // Skip annotation parameters if present diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 7d4fb068f..cfdc19811 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5171,6 +5171,13 @@ private int getContextualTokenType(char[] array, int start, int end, } if (isToken(array, start, end, "open")) { + // Be conservative: treat as keyword if we can't verify context (e.g., next line) + // This handles "open\nmodule" formatting + int next = findNextNonWhitespaceSkipComments(array, end + 1); + if (next < 0) { + // Next token not on this line - conservatively treat as keyword + return tokenType; + } return nextTokenEquals(array, end, "module") ? tokenType : TokenTypes.IDENTIFIER; } @@ -5349,14 +5356,14 @@ private boolean isModuleKeyword(char[] array, int start) { if (array[prev] == ')') { return true; } - // Check for bare annotation (e.g., @Deprecated module) - // Look for identifier preceded by @ + // Check for bare annotation (e.g., @Deprecated module) or qualified (e.g., @com.acme.Visible module) + // Look for identifier or qualified name preceded by @ if (Character.isJavaIdentifierPart(array[prev])) { int tokenStart = prev; - while (tokenStart > s.offset && Character.isJavaIdentifierPart(array[tokenStart - 1])) { + while (tokenStart > s.offset && (Character.isJavaIdentifierPart(array[tokenStart - 1]) || array[tokenStart - 1] == '.')) { tokenStart--; } - // Check if there's an @ before this identifier + // Check if there's an @ before this identifier/qualified name if (tokenStart > s.offset) { int beforeId = findPreviousNonWhitespace(array, tokenStart - 1); if (beforeId >= s.offset && array[beforeId] == '@') { @@ -5376,6 +5383,10 @@ private boolean isModuleKeyword(char[] array, int start) { */ private boolean nextClassOrInterfaceKeyword(char[] array, int end) { int pos = findNextNonWhitespaceSkipComments(array, end + 1); + // Conservative: if next token not on current line, treat as keyword to handle line breaks + if (pos < 0) { + return true; + } while (pos >= 0) { // Skip annotations (@...) if (array[pos] == '@') { @@ -5384,8 +5395,8 @@ private boolean nextClassOrInterfaceKeyword(char[] array, int end) { if (pos < 0 || !Character.isJavaIdentifierStart(array[pos])) { return false; } - // Skip the annotation name - while (pos < s.offset + s.count && Character.isJavaIdentifierPart(array[pos])) { + // Skip the annotation name (including qualified names like @com.acme.Anno) + while (pos < s.offset + s.count && (Character.isJavaIdentifierPart(array[pos]) || array[pos] == '.')) { pos++; } // Skip annotation parameters if present From 34bd958638431364b84cdb5226190bb8873e93d5 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 11:23:13 +0000 Subject: [PATCH 12/19] Handle multi-line module and yield statements conservatively Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/23bf6039-a16e-4c25-810b-01ab2a54865b Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../ui/rsyntaxtextarea/modes/JavaTokenMaker.flex | 15 ++++++++++----- .../ui/rsyntaxtextarea/modes/JavaTokenMaker.java | 15 ++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 1f4de84ce..15d3108af 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -98,9 +98,13 @@ import org.fife.ui.rsyntaxtextarea.*; } if (isToken(array, start, end, "module")) { - return nextTokenStartsIdentifier(array, end) && - isModuleKeyword(array, start) ? - tokenType : TokenTypes.IDENTIFIER; + // Be conservative: treat as keyword if we can't verify context (e.g., module name on next line) + // This handles "module\n com.example {}" formatting + if (!nextTokenStartsIdentifier(array, end)) { + // Next token not on this line or not an identifier - conservatively treat as keyword + return isModuleKeyword(array, start) ? tokenType : TokenTypes.IDENTIFIER; + } + return isModuleKeyword(array, start) ? tokenType : TokenTypes.IDENTIFIER; } if (isToken(array, start, end, "sealed") || @@ -228,8 +232,9 @@ import org.fife.ui.rsyntaxtextarea.*; private boolean isYieldKeywordAtLineStart(char[] array, int start) { int next = findNextNonWhitespaceSkipComments(array, start + "yield".length()); if (next < 0) { - // No token after yield - ambiguous, lean toward identifier - return false; + // No token after yield on this line - conservatively treat as keyword + // This handles "yield\n someValue" formatting + return true; } char ch = array[next]; // yield is a keyword if followed by a value/expression (including '(' for yield (expr)) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index cfdc19811..a7bca3760 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5183,9 +5183,13 @@ private int getContextualTokenType(char[] array, int start, int end, } if (isToken(array, start, end, "module")) { - return nextTokenStartsIdentifier(array, end) && - isModuleKeyword(array, start) ? - tokenType : TokenTypes.IDENTIFIER; + // Be conservative: treat as keyword if we can't verify context (e.g., module name on next line) + // This handles "module\n com.example {}" formatting + if (!nextTokenStartsIdentifier(array, end)) { + // Next token not on this line or not an identifier - conservatively treat as keyword + return isModuleKeyword(array, start) ? tokenType : TokenTypes.IDENTIFIER; + } + return isModuleKeyword(array, start) ? tokenType : TokenTypes.IDENTIFIER; } if (isToken(array, start, end, "sealed") || @@ -5313,8 +5317,9 @@ private boolean isToken(char[] array, int start, int end, String lexeme) { private boolean isYieldKeywordAtLineStart(char[] array, int start) { int next = findNextNonWhitespaceSkipComments(array, start + "yield".length()); if (next < 0) { - // No token after yield - ambiguous, lean toward identifier - return false; + // No token after yield on this line - conservatively treat as keyword + // This handles "yield\n someValue" formatting + return true; } char ch = array[next]; // yield is a keyword if followed by a value/expression (including '(' for yield (expr)) From e1ea33ab66d9296a9cb228cc561ee7446df2bfab Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 11:43:19 +0000 Subject: [PATCH 13/19] Recognize yield after else and fix multiline comment scanning Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/f834b1b6-347d-4be7-a575-d918a3c614d1 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 27 ++++++++++++++----- .../rsyntaxtextarea/modes/JavaTokenMaker.java | 27 ++++++++++++++----- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 15d3108af..4a2731df7 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -188,16 +188,24 @@ import org.fife.ui.rsyntaxtextarea.*; } // Skip multi-line comments (work backwards from */) if (pos > s.offset && array[pos] == '/' && array[pos - 1] == '*') { - // Found end of comment, skip backwards to find start + // Found end of comment, skip backwards to find matching start pos -= 2; - while (pos > s.offset) { - if (array[pos] == '*' && array[pos - 1] == '/') { + int depth = 1; // Track nesting depth + while (pos > s.offset && depth > 0) { + if (array[pos] == '/' && pos > s.offset && array[pos - 1] == '*') { + // Found another */ - increase depth + depth++; pos -= 2; - break; + } else if (array[pos] == '*' && pos > s.offset && array[pos - 1] == '/') { + // Found /* - decrease depth + depth--; + pos -= 2; + } else { + pos--; } - pos--; } - if (pos < s.offset) { + if (depth > 0 || pos < s.offset) { + // Didn't find matching start on this line return -1; } continue; @@ -248,12 +256,17 @@ import org.fife.ui.rsyntaxtextarea.*; if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword in statement contexts: after ':', '{', ';', or ')' + // yield is a keyword in statement contexts: after ':', '{', ';', ')', or 'else' // The ')' case handles control flow like "if (cond) yield value;" + // The 'else' case handles "if (c) yield 1; else yield 2;" char prevChar = array[prev]; if (prevChar == ':' || prevChar == '{' || prevChar == ';' || prevChar == ')') { return true; } + // Check if preceded by 'else' keyword + if (previousTokenEquals(array, start, "else")) { + return true; + } return false; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index a7bca3760..7a0d7fcd2 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5273,16 +5273,24 @@ private int findPreviousNonWhitespaceSkipComments(char[] array, int pos) { } // Skip multi-line comments (work backwards from */) if (pos > s.offset && array[pos] == '/' && array[pos - 1] == '*') { - // Found end of comment, skip backwards to find start + // Found end of comment, skip backwards to find matching start pos -= 2; - while (pos > s.offset) { - if (array[pos] == '*' && array[pos - 1] == '/') { + int depth = 1; // Track nesting depth + while (pos > s.offset && depth > 0) { + if (array[pos] == '/' && pos > s.offset && array[pos - 1] == '*') { + // Found another */ - increase depth + depth++; pos -= 2; - break; + } else if (array[pos] == '*' && pos > s.offset && array[pos - 1] == '/') { + // Found /* - decrease depth + depth--; + pos -= 2; + } else { + pos--; } - pos--; } - if (pos < s.offset) { + if (depth > 0 || pos < s.offset) { + // Didn't find matching start on this line return -1; } continue; @@ -5333,12 +5341,17 @@ private boolean isYieldKeyword(char[] array, int start) { if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword in statement contexts: after ':', '{', ';', or ')' + // yield is a keyword in statement contexts: after ':', '{', ';', ')', or 'else' // The ')' case handles control flow like "if (cond) yield value;" + // The 'else' case handles "if (c) yield 1; else yield 2;" char prevChar = array[prev]; if (prevChar == ':' || prevChar == '{' || prevChar == ';' || prevChar == ')') { return true; } + // Check if preceded by 'else' keyword + if (previousTokenEquals(array, start, "else")) { + return true; + } return false; } From 05c91eb35d6c78c0114375894086988a5895fcb6 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:21:07 +0000 Subject: [PATCH 14/19] Skip comments when detecting yield and module keywords Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/fd50eb05-6ed3-44f3-a0ea-d699cb66fe17 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex | 4 ++-- .../org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 4a2731df7..8513d8087 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -252,7 +252,7 @@ import org.fife.ui.rsyntaxtextarea.*; private boolean isYieldKeyword(char[] array, int start) { - int prev = findPreviousNonWhitespace(array, start - 1); + int prev = findPreviousNonWhitespaceSkipComments(array, start - 1); if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } @@ -419,7 +419,7 @@ import org.fife.ui.rsyntaxtextarea.*; private boolean previousTokenEquals(char[] array, int start, String lexeme) { - int prev = findPreviousNonWhitespace(array, start - 1); + int prev = findPreviousNonWhitespaceSkipComments(array, start - 1); if (prev < 0 || !Character.isJavaIdentifierPart(array[prev])) { return false; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 7a0d7fcd2..13866777f 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5337,7 +5337,7 @@ private boolean isYieldKeywordAtLineStart(char[] array, int start) { private boolean isYieldKeyword(char[] array, int start) { - int prev = findPreviousNonWhitespace(array, start - 1); + int prev = findPreviousNonWhitespaceSkipComments(array, start - 1); if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } @@ -5504,7 +5504,7 @@ private boolean noPreviousToken(char[] array, int start) { private boolean previousTokenEquals(char[] array, int start, String lexeme) { - int prev = findPreviousNonWhitespace(array, start - 1); + int prev = findPreviousNonWhitespaceSkipComments(array, start - 1); if (prev < 0 || !Character.isJavaIdentifierPart(array[prev])) { return false; } From 26e2a68ef389f60731f8136e20812d7162827de3 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:47:50 +0000 Subject: [PATCH 15/19] Recognize yield statements after block close Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/ab720ad5-3192-42c8-8551-80a7d99c7aa8 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex | 5 +++-- .../org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 8513d8087..78c5b85d8 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -256,11 +256,12 @@ import org.fife.ui.rsyntaxtextarea.*; if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword in statement contexts: after ':', '{', ';', ')', or 'else' + // yield is a keyword in statement contexts: after ':', '{', ';', ')', '}', or 'else' // The ')' case handles control flow like "if (cond) yield value;" + // The '}' case handles "case 0: { ... } yield 1;" // The 'else' case handles "if (c) yield 1; else yield 2;" char prevChar = array[prev]; - if (prevChar == ':' || prevChar == '{' || prevChar == ';' || prevChar == ')') { + if (prevChar == ':' || prevChar == '{' || prevChar == ';' || prevChar == ')' || prevChar == '}') { return true; } // Check if preceded by 'else' keyword diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 13866777f..b47a48c3e 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5341,11 +5341,12 @@ private boolean isYieldKeyword(char[] array, int start) { if (prev < s.offset) { return isYieldKeywordAtLineStart(array, start); } - // yield is a keyword in statement contexts: after ':', '{', ';', ')', or 'else' + // yield is a keyword in statement contexts: after ':', '{', ';', ')', '}', or 'else' // The ')' case handles control flow like "if (cond) yield value;" + // The '}' case handles "case 0: { ... } yield 1;" // The 'else' case handles "if (c) yield 1; else yield 2;" char prevChar = array[prev]; - if (prevChar == ':' || prevChar == '{' || prevChar == ';' || prevChar == ')') { + if (prevChar == ':' || prevChar == '{' || prevChar == ';' || prevChar == ')' || prevChar == '}') { return true; } // Check if preceded by 'else' keyword From 40b721501d006d5e3430e0b1e524cded69f4eee1 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 13:09:17 +0000 Subject: [PATCH 16/19] Fix unterminated block comment handling and skip comments in annotation detection Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/3f624baf-8728-45af-9b98-9e82bb0be852 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex | 10 ++++++++-- .../fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 78c5b85d8..3fdaf1a22 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -165,13 +165,19 @@ import org.fife.ui.rsyntaxtextarea.*; // Skip multi-line comments if (pos + 1 < end && array[pos] == '/' && array[pos + 1] == '*') { pos += 2; + boolean foundClosing = false; while (pos + 1 < end) { if (array[pos] == '*' && array[pos + 1] == '/') { pos += 2; + foundClosing = true; break; } pos++; } + // If comment is unterminated at end of line, treat as end-of-line + if (!foundClosing) { + return -1; + } continue; } return pos; @@ -284,7 +290,7 @@ import org.fife.ui.rsyntaxtextarea.*; if (previousTokenEquals(array, start, "open")) { return true; } - int prev = findPreviousNonWhitespace(array, start - 1); + int prev = findPreviousNonWhitespaceSkipComments(array, start - 1); if (prev >= s.offset) { // Check for closing paren of annotation if (array[prev] == ')') { @@ -299,7 +305,7 @@ import org.fife.ui.rsyntaxtextarea.*; } // Check if there's an @ before this identifier/qualified name if (tokenStart > s.offset) { - int beforeId = findPreviousNonWhitespace(array, tokenStart - 1); + int beforeId = findPreviousNonWhitespaceSkipComments(array, tokenStart - 1); if (beforeId >= s.offset && array[beforeId] == '@') { return true; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index b47a48c3e..57403f3bf 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5250,13 +5250,19 @@ private int findNextNonWhitespaceSkipComments(char[] array, int pos) { // Skip multi-line comments if (pos + 1 < end && array[pos] == '/' && array[pos + 1] == '*') { pos += 2; + boolean foundClosing = false; while (pos + 1 < end) { if (array[pos] == '*' && array[pos + 1] == '/') { pos += 2; + foundClosing = true; break; } pos++; } + // If comment is unterminated at end of line, treat as end-of-line + if (!foundClosing) { + return -1; + } continue; } return pos; @@ -5369,7 +5375,7 @@ private boolean isModuleKeyword(char[] array, int start) { if (previousTokenEquals(array, start, "open")) { return true; } - int prev = findPreviousNonWhitespace(array, start - 1); + int prev = findPreviousNonWhitespaceSkipComments(array, start - 1); if (prev >= s.offset) { // Check for closing paren of annotation if (array[prev] == ')') { @@ -5384,7 +5390,7 @@ private boolean isModuleKeyword(char[] array, int start) { } // Check if there's an @ before this identifier/qualified name if (tokenStart > s.offset) { - int beforeId = findPreviousNonWhitespace(array, tokenStart - 1); + int beforeId = findPreviousNonWhitespaceSkipComments(array, tokenStart - 1); if (beforeId >= s.offset && array[beforeId] == '@') { return true; } From e4f6a254f0a362d44f5aee404be533b64cc167b6 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 13:41:42 +0000 Subject: [PATCH 17/19] Skip strings and comments when scanning annotation parameters Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/c19648b6-504d-4197-9fa0-a8ddee806dbe Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 46 ++++++++++++++++++- .../rsyntaxtextarea/modes/JavaTokenMaker.java | 46 ++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 3fdaf1a22..0144928e7 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -342,10 +342,54 @@ import org.fife.ui.rsyntaxtextarea.*; // Skip annotation parameters if present pos = findNextNonWhitespaceSkipComments(array, pos); if (pos >= 0 && array[pos] == '(') { - // Skip to closing paren + // Skip to closing paren, handling strings and comments int depth = 1; pos++; while (pos < s.offset + s.count && depth > 0) { + // Skip string literals + if (array[pos] == '"') { + pos++; + while (pos < s.offset + s.count && array[pos] != '"') { + if (array[pos] == '\\' && pos + 1 < s.offset + s.count) { + pos += 2; // Skip escaped character + } else { + pos++; + } + } + pos++; // Skip closing quote + continue; + } + // Skip character literals + if (array[pos] == '\'') { + pos++; + while (pos < s.offset + s.count && array[pos] != '\'') { + if (array[pos] == '\\' && pos + 1 < s.offset + s.count) { + pos += 2; // Skip escaped character + } else { + pos++; + } + } + pos++; // Skip closing quote + continue; + } + // Skip single-line comments + if (pos + 1 < s.offset + s.count && array[pos] == '/' && array[pos + 1] == '/') { + // Skip to end of line + break; + } + // Skip multi-line comments + if (pos + 1 < s.offset + s.count && array[pos] == '/' && array[pos + 1] == '*') { + pos += 2; + while (pos + 1 < s.offset + s.count) { + if (array[pos] == '*' && array[pos + 1] == '/') { + pos += 2; + break; + } + pos++; + } + continue; + } + // Count parentheses if (array[pos] == '(') { depth++; } else if (array[pos] == ')') { diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 57403f3bf..4237955e3 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5427,10 +5427,54 @@ private boolean nextClassOrInterfaceKeyword(char[] array, int end) { // Skip annotation parameters if present pos = findNextNonWhitespaceSkipComments(array, pos); if (pos >= 0 && array[pos] == '(') { - // Skip to closing paren + // Skip to closing paren, handling strings and comments int depth = 1; pos++; while (pos < s.offset + s.count && depth > 0) { + // Skip string literals + if (array[pos] == '"') { + pos++; + while (pos < s.offset + s.count && array[pos] != '"') { + if (array[pos] == '\\' && pos + 1 < s.offset + s.count) { + pos += 2; // Skip escaped character + } else { + pos++; + } + } + pos++; // Skip closing quote + continue; + } + // Skip character literals + if (array[pos] == '\'') { + pos++; + while (pos < s.offset + s.count && array[pos] != '\'') { + if (array[pos] == '\\' && pos + 1 < s.offset + s.count) { + pos += 2; // Skip escaped character + } else { + pos++; + } + } + pos++; // Skip closing quote + continue; + } + // Skip single-line comments + if (pos + 1 < s.offset + s.count && array[pos] == '/' && array[pos + 1] == '/') { + // Skip to end of line + break; + } + // Skip multi-line comments + if (pos + 1 < s.offset + s.count && array[pos] == '/' && array[pos + 1] == '*') { + pos += 2; + while (pos + 1 < s.offset + s.count) { + if (array[pos] == '*' && array[pos + 1] == '/') { + pos += 2; + break; + } + pos++; + } + continue; + } + // Count parentheses if (array[pos] == '(') { depth++; } else if (array[pos] == ')') { From 7872a334f73d9549203cb35c41c23e7754e94d4d Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 14:04:30 +0000 Subject: [PATCH 18/19] Guard against negative position and handle wrapped annotation parameters Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/dff16ace-6da4-4fd1-acc1-3f5f911291c8 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex | 9 ++++++++- .../fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 0144928e7..70512041e 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -397,8 +397,15 @@ import org.fife.ui.rsyntaxtextarea.*; } pos++; } + // If annotation parameters wrap lines, treat conservatively as keyword + if (depth > 0) { + return true; + } + } + // Continue to next token after annotation (guard against negative pos) + if (pos < 0) { + return true; } - // Continue to next token after annotation pos = findNextNonWhitespaceSkipComments(array, pos); continue; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 4237955e3..918013e27 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5482,8 +5482,15 @@ private boolean nextClassOrInterfaceKeyword(char[] array, int end) { } pos++; } + // If annotation parameters wrap lines, treat conservatively as keyword + if (depth > 0) { + return true; + } + } + // Continue to next token after annotation (guard against negative pos) + if (pos < 0) { + return true; } - // Continue to next token after annotation pos = findNextNonWhitespaceSkipComments(array, pos); continue; } From aedf1bda24b28c402b60553570aa334427b29a59 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 18 Apr 2026 14:34:35 +0000 Subject: [PATCH 19/19] Treat Java block comments as non-nestable in reverse scan Agent-Logs-Url: https://github.com/nbauma109/RSyntaxTextArea/sessions/65720246-2da3-47e6-8bb0-ca8e9057c990 Co-authored-by: nbauma109 <9403560+nbauma109@users.noreply.github.com> --- .../rsyntaxtextarea/modes/JavaTokenMaker.flex | 19 +++++++------------ .../rsyntaxtextarea/modes/JavaTokenMaker.java | 19 +++++++------------ 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex index 70512041e..9c1c00c44 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.flex @@ -195,22 +195,17 @@ import org.fife.ui.rsyntaxtextarea.*; // Skip multi-line comments (work backwards from */) if (pos > s.offset && array[pos] == '/' && array[pos - 1] == '*') { // Found end of comment, skip backwards to find matching start + // Java block comments don't nest, so find the first /* pos -= 2; - int depth = 1; // Track nesting depth - while (pos > s.offset && depth > 0) { - if (array[pos] == '/' && pos > s.offset && array[pos - 1] == '*') { - // Found another */ - increase depth - depth++; + while (pos > s.offset) { + if (array[pos] == '*' && pos > s.offset && array[pos - 1] == '/') { + // Found /* - this is the start of the comment pos -= 2; - } else if (array[pos] == '*' && pos > s.offset && array[pos - 1] == '/') { - // Found /* - decrease depth - depth--; - pos -= 2; - } else { - pos--; + break; } + pos--; } - if (depth > 0 || pos < s.offset) { + if (pos < s.offset) { // Didn't find matching start on this line return -1; } diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java index 918013e27..ee59643d7 100644 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/modes/JavaTokenMaker.java @@ -5280,22 +5280,17 @@ private int findPreviousNonWhitespaceSkipComments(char[] array, int pos) { // Skip multi-line comments (work backwards from */) if (pos > s.offset && array[pos] == '/' && array[pos - 1] == '*') { // Found end of comment, skip backwards to find matching start + // Java block comments don't nest, so find the first /* pos -= 2; - int depth = 1; // Track nesting depth - while (pos > s.offset && depth > 0) { - if (array[pos] == '/' && pos > s.offset && array[pos - 1] == '*') { - // Found another */ - increase depth - depth++; + while (pos > s.offset) { + if (array[pos] == '*' && pos > s.offset && array[pos - 1] == '/') { + // Found /* - this is the start of the comment pos -= 2; - } else if (array[pos] == '*' && pos > s.offset && array[pos - 1] == '/') { - // Found /* - decrease depth - depth--; - pos -= 2; - } else { - pos--; + break; } + pos--; } - if (depth > 0 || pos < s.offset) { + if (pos < s.offset) { // Didn't find matching start on this line return -1; }