This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-23648-run-folder in repository https://gitbox.apache.org/repos/asf/camel.git
commit ad88468df388ff77fe943a68c70e1ade2caa5055 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Jun 1 09:40:26 2026 +0200 CAMEL-23648: camel-jbang - TUI source code syntax highlighting Co-Authored-By: Claude <[email protected]> --- .../dsl/jbang/core/commands/tui/RoutesTab.java | 51 +++- .../jbang/core/commands/tui/SyntaxHighlighter.java | 291 +++++++++++++++++++++ 2 files changed, 340 insertions(+), 2 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java index 0e363d62e26f..48de04ec6473 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java @@ -44,6 +44,8 @@ import dev.tamboui.widgets.table.Row; import dev.tamboui.widgets.table.Table; import dev.tamboui.widgets.table.TableState; import org.apache.camel.dsl.jbang.core.common.PathUtils; +import org.apache.camel.support.LoggerHelper; +import org.apache.camel.util.FileUtil; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; import org.apache.camel.util.json.Jsoner; @@ -81,6 +83,7 @@ class RoutesTab implements MonitorTab { private boolean showSource; private List<String> sourceLines = Collections.emptyList(); private String sourceTitle; + private SyntaxHighlighter.Language sourceLanguage = SyntaxHighlighter.Language.PLAIN; private int sourceScroll; private int sourceScrollX; private final ScrollbarState sourceVScrollState = new ScrollbarState(); @@ -732,7 +735,7 @@ class RoutesTab implements MonitorTab { List<Line> visible = new ArrayList<>(); for (int i = sourceScroll; i < end; i++) { String raw = sourceLines.get(i); - visible.add(TuiHelper.ansiToLine(raw, sourceScrollX)); + visible.add(highlightSourceLine(raw, sourceScrollX)); } frame.renderWidget(Paragraph.builder().text(Text.from(visible)).build(), inner); @@ -748,6 +751,48 @@ class RoutesTab implements MonitorTab { } } + private Line highlightSourceLine(String raw, int hSkip) { + // Split line number prefix from code content + int prefixEnd = 0; + while (prefixEnd < raw.length() && (raw.charAt(prefixEnd) == ' ' || Character.isDigit(raw.charAt(prefixEnd)))) { + prefixEnd++; + } + + String prefix = raw.substring(0, prefixEnd); + String code = raw.substring(prefixEnd); + + Line highlighted = SyntaxHighlighter.highlightLine(code, sourceLanguage); + + // Prepend dim line-number prefix + List<Span> spans = new ArrayList<>(); + if (!prefix.isEmpty()) { + spans.add(Span.styled(prefix, Style.EMPTY.dim())); + } + spans.addAll(highlighted.spans()); + + Line full = Line.from(spans); + + // Apply horizontal scroll by skipping characters from spans + if (hSkip <= 0) { + return full; + } + List<Span> scrolled = new ArrayList<>(); + int skipped = 0; + for (Span span : full.spans()) { + String content = span.content(); + if (skipped >= hSkip) { + scrolled.add(span); + } else if (skipped + content.length() > hSkip) { + int offset = hSkip - skipped; + scrolled.add(Span.styled(content.substring(offset), span.style())); + skipped = hSkip; + } else { + skipped += content.length(); + } + } + return scrolled.isEmpty() ? Line.from(List.of(Span.raw(""))) : Line.from(scrolled); + } + // ---- Sorting ---- private int sortRoute(RouteInfo a, RouteInfo b) { @@ -1081,7 +1126,9 @@ class RoutesTab implements MonitorTab { if (!showSource) { return; } - sourceTitle = location != null ? routeId + " " + location : routeId; + String displayLoc = location != null ? FileUtil.stripPath(LoggerHelper.sourceNameOnly(location)) : null; + sourceTitle = displayLoc != null ? routeId + " " + displayLoc : routeId; + sourceLanguage = SyntaxHighlighter.detectLanguage(location); sourceLines = lines; sourceScroll = scrollTo; }); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java new file mode 100644 index 000000000000..dbd3b68ccdd2 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java @@ -0,0 +1,291 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.tui; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import dev.tamboui.style.Color; +import dev.tamboui.style.Style; +import dev.tamboui.text.Line; +import dev.tamboui.text.Span; +import org.apache.camel.util.FileUtil; + +class SyntaxHighlighter { + + enum Language { + JAVA, + YAML, + XML, + PLAIN + } + + // Java patterns (ordered by priority — comments first) + private static final Pattern JAVA_LINE_COMMENT = Pattern.compile("//.*"); + private static final Pattern JAVA_STRING = Pattern.compile("\"(?:[^\"\\\\]|\\\\.)*\""); + private static final Pattern JAVA_ANNOTATION = Pattern.compile("@\\w+"); + private static final Pattern JAVA_MODIFIER = Pattern.compile( + "\\b(abstract|class|extends|final|implements|import|instanceof|interface|native|package|private|protected|public|static|strictfp|super|synchronized|throws|volatile|enum|record|sealed|permits|non-sealed)\\b"); + private static final Pattern JAVA_KEYWORD = Pattern.compile( + "\\b(break|case|catch|continue|default|do|else|finally|for|if|return|switch|throw|try|while|yield|var)\\b"); + private static final Pattern JAVA_TYPE = Pattern.compile( + "\\b(boolean|byte|char|double|float|int|long|new|short|this|transient|void)\\b"); + private static final Pattern JAVA_BOOLEAN_NULL = Pattern.compile("\\b(true|false|null)\\b"); + private static final Pattern JAVA_NUMBER = Pattern.compile("\\b\\d+\\.?\\d*[fFdDlL]?\\b"); + + // YAML patterns + private static final Pattern YAML_COMMENT = Pattern.compile("(^|\\s)#.*$"); + private static final Pattern YAML_KEY = Pattern.compile("^(\\s*-?\\s*)([\\w./${}\\-]+)\\s*:"); + private static final Pattern YAML_BOOLEAN_NULL = Pattern.compile(":\\s+(true|false|null)\\s*$"); + private static final Pattern YAML_NUMBER = Pattern.compile(":\\s+(\\d+\\.?\\d*)\\s*$"); + private static final Pattern YAML_STRING_VALUE = Pattern.compile("\"(?:[^\"\\\\]|\\\\.)*\"|'[^']*'"); + + // XML patterns + private static final Pattern XML_COMMENT = Pattern.compile("<!--.*?-->"); + private static final Pattern XML_OPEN_TAG = Pattern.compile("</?[\\w:.-]+"); + private static final Pattern XML_CLOSE_BRACKET = Pattern.compile("/?>|>"); + private static final Pattern XML_ATTR_VALUE = Pattern.compile("=\"[^\"]*\""); + private static final Pattern XML_ATTR_NAME = Pattern.compile("\\s([\\w:.-]+)="); + private static final Pattern XML_ENTITY = Pattern.compile("&[^;]+;"); + + // Java styles + private static final Style JAVA_COMMENT_STYLE = Style.EMPTY.fg(Color.LIGHT_BLUE); + private static final Style JAVA_STRING_STYLE = Style.EMPTY.fg(Color.RED); + private static final Style JAVA_ANNOTATION_STYLE = Style.EMPTY.fg(Color.MAGENTA); + private static final Style JAVA_MODIFIER_STYLE = Style.EMPTY.fg(Color.CYAN); + private static final Style JAVA_KEYWORD_STYLE = Style.EMPTY.fg(Color.RED); + private static final Style JAVA_TYPE_STYLE = Style.EMPTY.fg(Color.GREEN); + private static final Style JAVA_BOOLEAN_STYLE = Style.EMPTY.fg(Color.YELLOW); + private static final Style JAVA_NUMBER_STYLE = Style.EMPTY.fg(Color.YELLOW); + + // YAML styles + private static final Style YAML_COMMENT_STYLE = Style.EMPTY.fg(Color.LIGHT_BLUE); + private static final Style YAML_KEY_STYLE = Style.EMPTY.fg(Color.RED); + private static final Style YAML_VALUE_STYLE = Style.EMPTY.fg(Color.GREEN); + private static final Style YAML_SPECIAL_STYLE = Style.EMPTY.fg(Color.YELLOW); + private static final Style YAML_SEPARATOR_STYLE = Style.EMPTY.fg(Color.WHITE).bold(); + + // XML styles + private static final Style XML_COMMENT_STYLE = Style.EMPTY.fg(Color.YELLOW); + private static final Style XML_TAG_STYLE = Style.EMPTY.fg(Color.CYAN); + private static final Style XML_ATTR_NAME_STYLE = Style.EMPTY.fg(Color.MAGENTA); + private static final Style XML_ATTR_VALUE_STYLE = Style.EMPTY.fg(Color.GREEN); + private static final Style XML_ENTITY_STYLE = Style.EMPTY.fg(Color.RED); + + private SyntaxHighlighter() { + } + + static Language detectLanguage(String filename) { + if (filename == null || filename.isEmpty()) { + return Language.PLAIN; + } + // Strip line number suffixes (e.g., "MyRoute.java:42") + String name = filename; + int colon = name.lastIndexOf(':'); + if (colon > 0) { + String after = name.substring(colon + 1); + if (!after.isEmpty() && after.chars().allMatch(Character::isDigit)) { + name = name.substring(0, colon); + } + } + String ext = FileUtil.onlyExt(name); + if (ext == null) { + return Language.PLAIN; + } + ext = ext.toLowerCase(); + return switch (ext) { + case "java" -> Language.JAVA; + case "yaml", "yml", "camel.yaml", "camel.yml" -> Language.YAML; + case "xml", "camel.xml" -> Language.XML; + default -> Language.PLAIN; + }; + } + + static Line highlightLine(String text, Language lang) { + if (text == null || text.isEmpty() || lang == Language.PLAIN) { + return Line.from(List.of(Span.raw(text != null ? text : ""))); + } + + return switch (lang) { + case JAVA -> highlightJava(text); + case YAML -> highlightYaml(text); + case XML -> highlightXml(text); + default -> Line.from(List.of(Span.raw(text))); + }; + } + + private static Line highlightJava(String text) { + int len = text.length(); + Style[] charStyles = new Style[len]; + + // Priority order: comments > strings > annotations > keywords > numbers + applyPattern(charStyles, text, JAVA_LINE_COMMENT, JAVA_COMMENT_STYLE); + applyPattern(charStyles, text, JAVA_STRING, JAVA_STRING_STYLE); + applyPattern(charStyles, text, JAVA_ANNOTATION, JAVA_ANNOTATION_STYLE); + applyPattern(charStyles, text, JAVA_MODIFIER, JAVA_MODIFIER_STYLE); + applyPattern(charStyles, text, JAVA_KEYWORD, JAVA_KEYWORD_STYLE); + applyPattern(charStyles, text, JAVA_TYPE, JAVA_TYPE_STYLE); + applyPattern(charStyles, text, JAVA_BOOLEAN_NULL, JAVA_BOOLEAN_STYLE); + applyPattern(charStyles, text, JAVA_NUMBER, JAVA_NUMBER_STYLE); + + return buildLine(text, charStyles); + } + + private static Line highlightYaml(String text) { + int len = text.length(); + Style[] charStyles = new Style[len]; + + // Comments have highest priority + applyPattern(charStyles, text, YAML_COMMENT, YAML_COMMENT_STYLE); + + // Key portion (before colon) + Matcher keyMatcher = YAML_KEY.matcher(text); + if (keyMatcher.find()) { + int keyStart = keyMatcher.start(2); + int keyEnd = keyMatcher.end(2); + for (int i = keyStart; i < keyEnd && i < len; i++) { + if (charStyles[i] == null) { + charStyles[i] = YAML_KEY_STYLE; + } + } + // Colon separator + int colonIdx = text.indexOf(':', keyEnd); + if (colonIdx >= 0 && colonIdx < len && charStyles[colonIdx] == null) { + charStyles[colonIdx] = YAML_SEPARATOR_STYLE; + } + } + + // String values + applyPattern(charStyles, text, YAML_STRING_VALUE, YAML_VALUE_STYLE); + + // Special values (boolean, null, numbers) after colon + applyPatternGroup(charStyles, text, YAML_BOOLEAN_NULL, 1, YAML_SPECIAL_STYLE); + applyPatternGroup(charStyles, text, YAML_NUMBER, 1, YAML_SPECIAL_STYLE); + + // List markers + Matcher listMarker = Pattern.compile("^(\\s*)(-)(\\s)").matcher(text); + if (listMarker.find()) { + int dashIdx = listMarker.start(2); + if (dashIdx < len && charStyles[dashIdx] == null) { + charStyles[dashIdx] = YAML_SEPARATOR_STYLE; + } + } + + // Value text (after colon+space, non-special, non-quoted) + int colonPos = text.indexOf(':'); + if (colonPos >= 0 && colonPos + 1 < len) { + int valueStart = colonPos + 1; + while (valueStart < len && text.charAt(valueStart) == ' ') { + valueStart++; + } + if (valueStart < len) { + boolean hasSpecial = false; + for (int i = valueStart; i < len; i++) { + if (charStyles[i] != null) { + hasSpecial = true; + break; + } + } + if (!hasSpecial) { + for (int i = valueStart; i < len; i++) { + charStyles[i] = YAML_VALUE_STYLE; + } + } + } + } + + return buildLine(text, charStyles); + } + + private static Line highlightXml(String text) { + int len = text.length(); + Style[] charStyles = new Style[len]; + + // Comments highest priority + applyPattern(charStyles, text, XML_COMMENT, XML_COMMENT_STYLE); + + // Attribute values (before tag names so tags don't override) + applyPattern(charStyles, text, XML_ATTR_VALUE, XML_ATTR_VALUE_STYLE); + + // Attribute names + Matcher attrMatcher = XML_ATTR_NAME.matcher(text); + while (attrMatcher.find()) { + int start = attrMatcher.start(1); + int end = attrMatcher.end(1); + for (int i = start; i < end; i++) { + if (charStyles[i] == null) { + charStyles[i] = XML_ATTR_NAME_STYLE; + } + } + } + + // Tag names + applyPattern(charStyles, text, XML_OPEN_TAG, XML_TAG_STYLE); + applyPattern(charStyles, text, XML_CLOSE_BRACKET, XML_TAG_STYLE); + + // Entity references + applyPattern(charStyles, text, XML_ENTITY, XML_ENTITY_STYLE); + + return buildLine(text, charStyles); + } + + private static void applyPattern(Style[] charStyles, String text, Pattern pattern, Style style) { + Matcher m = pattern.matcher(text); + while (m.find()) { + for (int i = m.start(); i < m.end(); i++) { + if (charStyles[i] == null) { + charStyles[i] = style; + } + } + } + } + + private static void applyPatternGroup(Style[] charStyles, String text, Pattern pattern, int group, Style style) { + Matcher m = pattern.matcher(text); + while (m.find()) { + for (int i = m.start(group); i < m.end(group); i++) { + if (charStyles[i] == null) { + charStyles[i] = style; + } + } + } + } + + private static Line buildLine(String text, Style[] charStyles) { + List<Span> spans = new ArrayList<>(); + int len = text.length(); + int i = 0; + + while (i < len) { + Style current = charStyles[i]; + int start = i; + while (i < len && charStyles[i] == current) { + i++; + } + String segment = text.substring(start, i); + if (current != null) { + spans.add(Span.styled(segment, current)); + } else { + spans.add(Span.raw(segment)); + } + } + + return spans.isEmpty() ? Line.from(List.of(Span.raw(text))) : Line.from(spans); + } +}
