This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 548d19eb358e CAMEL-25219: camel-jbang - the quick doc shows the simple
function the cursor is on (#27234)
548d19eb358e is described below
commit 548d19eb358eac2c8ac13079a0977553f5901999
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 22:59:07 2026 +0200
CAMEL-25219: camel-jbang - the quick doc shows the simple function the
cursor is on (#27234)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../jbang/camel-tui-source-simple-quick-doc.png | Bin 0 -> 274197 bytes
.../ROOT/pages/camel-jbang-tui-source-editor.adoc | 8 +
.../core/commands/tui/SimpleCompletionContext.java | 49 ++++
.../jbang/core/commands/tui/SimpleCompletions.java | 9 +-
.../jbang/core/commands/tui/SimpleQuickDoc.java | 271 +++++++++++++++++++++
.../jbang/core/commands/tui/SourceEditAssist.java | 5 +
.../dsl/jbang/core/commands/tui/SourceTab.java | 2 +
.../dsl/jbang/core/commands/tui/SourceViewer.java | 43 +++-
.../src/main/resources/tui/help/source.md | 3 +-
.../core/commands/tui/SimpleQuickDocTest.java | 124 ++++++++++
10 files changed, 510 insertions(+), 4 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-simple-quick-doc.png
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-simple-quick-doc.png
new file mode 100644
index 000000000000..6906c1a3244a
Binary files /dev/null and
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-simple-quick-doc.png
differ
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index a775c0c57404..7593528cf780 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -154,6 +154,14 @@ project declares it (`@BindToRegistry`, `@Named`,
`@Component`, `@Bean`, or the
image::jbang/camel-tui-source-bean-jump.png[Where the bean of a line is
declared, with a jump link to it]
+In a xref:components:languages:simple-language.adoc[Simple] expression, the
panel follows the cursor within the line: on
+a function it shows what the function is, an example and its parameters; on
`+${header.priority}+` the header too (set
+in this file, or the description of a component header such as
`CamelKafkaKey`); on an operator after a function, such
+as `contains`, the operator and its syntax. Elsewhere on the line it shows the
documentation of the line as usual, and
+outside edit mode the line lists the functions it uses.
+
+image::jbang/camel-tui-source-simple-quick-doc.png[The documentation of the
date function the cursor is on]
+
== Navigation
* *Jump links* -- a line that sends to another route, such as
`to("seda:shipping")`, shows `↵ shipping`. Press *Enter*
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionContext.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionContext.java
index c808e6cb9482..21b1768896d6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionContext.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionContext.java
@@ -107,6 +107,55 @@ record SimpleCompletionContext(Kind kind, String prefix,
String closing, List<St
return new SimpleCompletionContext(Kind.OPERATOR, m.group(1), " ",
owners(lines, row, firstOpen));
}
+ /**
+ * A function of a simple expression on a line: the text between ${ and }
(to the end of the line when it is not
+ * closed yet), nested functions included.
+ *
+ * @param text the text of the function, such as date:now-24h or
header.priority
+ * @param start where the text starts in the line, after the ${
+ */
+ record Function(String text, int start) {
+ }
+
+ /**
+ * The function the cursor is on, the innermost one when they are nested
(${abs(${header.price})}), the ${ and }
+ * included; null when the cursor is not in one.
+ */
+ static Function functionAt(String line, int col) {
+ Function best = null;
+ for (Function f : functions(line)) {
+ int end = f.start() + f.text().length();
+ if (col >= f.start() - 2 && col <= end && (best == null ||
f.start() > best.start())) {
+ best = f;
+ }
+ }
+ return best;
+ }
+
+ /** The functions of a line, in the order they start. */
+ static List<Function> functions(String line) {
+ List<Function> found = new ArrayList<>();
+ List<Integer> open = new ArrayList<>();
+ List<Integer> slots = new ArrayList<>();
+ for (int i = 0; i < line.length(); i++) {
+ char c = line.charAt(i);
+ if (c == '$' && i + 1 < line.length() && line.charAt(i + 1) ==
'{') {
+ open.add(i + 2);
+ slots.add(found.size());
+ found.add(null);
+ i++;
+ } else if (c == '}' && !open.isEmpty()) {
+ int start = open.remove(open.size() - 1);
+ found.set(slots.remove(slots.size() - 1), new
Function(line.substring(start, i), start));
+ }
+ }
+ // the ones still open run to the end of the line, being typed
+ for (int k = 0; k < open.size(); k++) {
+ found.set(slots.get(k), new Function(line.substring(open.get(k)),
open.get(k)));
+ }
+ return found;
+ }
+
/** The words before the expression that starts at col of row, nearest
first. */
private static List<String> owners(List<String> lines, int row, int col) {
List<String> owners = new ArrayList<>();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletions.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletions.java
index b43640d1d7fc..afd772196ecd 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletions.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletions.java
@@ -224,9 +224,14 @@ final class SimpleCompletions {
continue;
}
for (ComponentModel.EndpointHeaderModel h :
model.getEndpointHeaders()) {
- items.putIfAbsent(h.getName(), new
AutocompletePopup.CompletionItem(
+ AutocompletePopup.CompletionItem item = new
AutocompletePopup.CompletionItem(
h.getName(), h.getDescription(), h.getJavaType(),
h.getDefaultValue(),
- h.isDeprecated(), h.getDeprecationNote(), scheme,
false));
+ h.isDeprecated(), h.getDeprecationNote(), scheme,
false);
+ // one the file uses keeps its place first, with the
documentation of the component
+ AutocompletePopup.CompletionItem known =
items.get(h.getName());
+ if (known == null || known.group() == null) {
+ items.put(h.getName(), item);
+ }
}
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleQuickDoc.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleQuickDoc.java
new file mode 100644
index 000000000000..3c04fdef68f6
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleQuickDoc.java
@@ -0,0 +1,271 @@
+/*
+ * 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.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.tooling.model.LanguageModel;
+
+/**
+ * The quick doc of the simple function the cursor is on (CAMEL-25219): what
it is, an example and its parameters, from
+ * the catalog; for ${header.x} the header too, and for an operator after a
function the operator. Read from the line
+ * alone, as the completion is, so it works on a line being typed. Drawn on
every render, so the catalog's simple model
+ * and the map from the written form of a function to it are kept per catalog.
+ */
+final class SimpleQuickDoc {
+
+ static final String TITLE = "Simple";
+
+ // the simple model of the last catalog and its functions by how they are
written: body, header., date:, random(
+ private static volatile Model model;
+
+ private record Model(
+ CamelCatalog catalog, LanguageModel simple, Map<String,
List<LanguageModel.LanguageFunctionModel>> written) {
+ }
+
+ private SimpleQuickDoc() {
+ }
+
+ /**
+ * The doc of what the cursor is on: col of row; col -1 (the view, no
cursor) gives the functions of the line in one
+ * entry.
+ */
+ static List<SourceViewer.DocEntry> at(CamelCatalog catalog, List<String>
lines, int row, int col) {
+ if (catalog == null || lines == null || row < 0 || row >=
lines.size()) {
+ return List.of();
+ }
+ String line = lines.get(row);
+ if (line.indexOf("${") < 0) {
+ return List.of();
+ }
+ Model m = model(catalog);
+ if (m == null) {
+ return List.of();
+ }
+ if (col < 0) {
+ return summary(m, line);
+ }
+ SimpleCompletionContext.Function f =
SimpleCompletionContext.functionAt(line, col);
+ if (f == null) {
+ return operatorAt(m, line, col);
+ }
+ LanguageModel.LanguageFunctionModel fn = resolve(m, f.text());
+ if (fn == null) {
+ return List.of();
+ }
+ List<SourceViewer.DocEntry> entries = new ArrayList<>();
+ String insert = SimpleCompletions.insertOf(fn);
+ String name = SimpleCompletions.displayOf(fn.getName(), insert);
+ String head = name + (fn.getJavaType() != null ? " — " +
simpleType(fn.getJavaType()) : "") + " — "
+ + firstSentence(fn.getDescription());
+ entries.add(new SourceViewer.DocEntry(head, fn.isDeprecated(), TITLE));
+ SourceViewer.DocEntry named = nameDoc(catalog, lines, insert,
f.text());
+ if (named != null) {
+ entries.add(named);
+ }
+ if (!fn.getExamples().isEmpty()) {
+ entries.add(new SourceViewer.DocEntry("Example: " +
fn.getExamples().get(0), false, TITLE));
+ }
+ if (!fn.getParams().isEmpty()) {
+ List<String> params = new ArrayList<>();
+ for (LanguageModel.FunctionParamModel p : fn.getParams()) {
+ params.add(p.getName() + (p.isRequired() ? " (required)" : "")
+ + (p.getDescription() != null ? ": " +
firstSentence(p.getDescription()) : ""));
+ }
+ entries.add(new SourceViewer.DocEntry(String.join("; ", params),
false, TITLE));
+ }
+ return entries;
+ }
+
+ /** The function a written one is: the longest written form it starts
with, by its number of arguments. */
+ static LanguageModel.LanguageFunctionModel resolve(CamelCatalog catalog,
String text) {
+ Model m = model(catalog);
+ return m != null ? resolve(m, text) : null;
+ }
+
+ private static LanguageModel.LanguageFunctionModel resolve(Model m, String
text) {
+ String t = text.strip();
+ List<LanguageModel.LanguageFunctionModel> candidates =
m.written().get(t);
+ if (candidates == null) {
+ String best = null;
+ for (String w : m.written().keySet()) {
+ boolean stem = w.endsWith(".") || w.endsWith(":") ||
w.endsWith("(") || w.endsWith("[");
+ if (stem && t.startsWith(w) && (best == null || w.length() >
best.length())) {
+ best = w;
+ }
+ }
+ if (best == null) {
+ // a function used with OGNL after it: ${body.length},
${exception.message.trim}
+ int dot = t.indexOf('.');
+ candidates = dot > 0 ? m.written().get(t.substring(0, dot)) :
null;
+ } else {
+ candidates = m.written().get(best);
+ }
+ }
+ if (candidates == null || candidates.isEmpty()) {
+ return null;
+ }
+ if (candidates.size() > 1) {
+ int args = arguments(t);
+ for (LanguageModel.LanguageFunctionModel c : candidates) {
+ if (c.getParams().size() == args) {
+ return c;
+ }
+ }
+ }
+ return candidates.get(0);
+ }
+
+ /** The number of arguments given in the (...) of a written function,
nested ones not counted. */
+ private static int arguments(String text) {
+ int open = text.indexOf('(');
+ if (open < 0) {
+ return 0;
+ }
+ int depth = 0;
+ int args = 1;
+ boolean quoted = false;
+ for (int i = open + 1; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (c == '\'') {
+ quoted = !quoted;
+ } else if (quoted) {
+ continue;
+ } else if (c == '(' || c == '{') {
+ depth++;
+ } else if ((c == ')' || c == '}') && depth > 0) {
+ depth--;
+ } else if (c == ')') {
+ break;
+ } else if (c == ',' && depth == 0) {
+ args++;
+ }
+ }
+ return args;
+ }
+
+ /** The header, exchange property or variable after header.
exchangeProperty. variable. */
+ private static SourceViewer.DocEntry nameDoc(CamelCatalog catalog,
List<String> lines, String insert, String text) {
+ String stripped = text.strip();
+ String name = stripped.substring(Math.min(insert.length(),
stripped.length()));
+ if (name.isEmpty()) {
+ return null;
+ }
+ List<AutocompletePopup.CompletionItem> known = switch (insert) {
+ case "header." -> SimpleCompletions.names(catalog, lines,
"Header", "header", true);
+ case "exchangeProperty." -> SimpleCompletions.names(catalog,
lines, "Property", "exchangeProperty", false);
+ case "variable." -> SimpleCompletions.names(catalog, lines,
"Variable", "variable", false);
+ default -> List.of();
+ };
+ for (AutocompletePopup.CompletionItem item : known) {
+ if (item.key().equals(name)) {
+ String where = item.group() != null ? item.group() + " — " :
"";
+ return new SourceViewer.DocEntry(name + " — " + where +
firstSentence(item.description()), false, TITLE);
+ }
+ }
+ return null;
+ }
+
+ /** The operator the cursor is on, after a function: ${body} contai|ns
'x'. */
+ private static List<SourceViewer.DocEntry> operatorAt(Model m, String
line, int col) {
+ int from = col;
+ while (from > 0 && !Character.isWhitespace(line.charAt(from - 1))) {
+ from--;
+ }
+ int to = col;
+ while (to < line.length() && !Character.isWhitespace(line.charAt(to)))
{
+ to++;
+ }
+ if (from >= to || !line.substring(0,
from).stripTrailing().endsWith("}")) {
+ return List.of();
+ }
+ String word = line.substring(from, to);
+ for (LanguageModel.LanguageOperatorModel op :
m.simple().getOperators()) {
+ if (op.getName().equals(word)) {
+ List<SourceViewer.DocEntry> entries = new ArrayList<>();
+ entries.add(new SourceViewer.DocEntry(
+ op.getName() + " — " + op.getDisplayName() + " — " +
firstSentence(op.getDescription()),
+ op.isDeprecated(), TITLE));
+ if (op.getOperatorSyntax() != null) {
+ entries.add(new SourceViewer.DocEntry("Syntax: " +
op.getOperatorSyntax(), false, TITLE));
+ }
+ if (!op.getExamples().isEmpty()) {
+ entries.add(new SourceViewer.DocEntry("Example: " +
op.getExamples().get(0), false, TITLE));
+ }
+ return entries;
+ }
+ }
+ return List.of();
+ }
+
+ /** The functions a line uses, in one entry: date:command, header.name. */
+ private static List<SourceViewer.DocEntry> summary(Model m, String line) {
+ Set<String> names = new LinkedHashSet<>();
+ for (SimpleCompletionContext.Function f :
SimpleCompletionContext.functions(line)) {
+ LanguageModel.LanguageFunctionModel fn = resolve(m, f.text());
+ if (fn != null) {
+ names.add(SimpleCompletions.displayOf(fn.getName(),
SimpleCompletions.insertOf(fn)));
+ }
+ }
+ if (names.isEmpty()) {
+ return List.of();
+ }
+ return List.of(new SourceViewer.DocEntry("Simple functions: " +
String.join(", ", names), false, TITLE));
+ }
+
+ private static Model model(CamelCatalog catalog) {
+ Model m = model;
+ if (m == null || m.catalog() != catalog) {
+ LanguageModel simple = catalog.languageModel("simple");
+ if (simple == null) {
+ return null;
+ }
+ Map<String, List<LanguageModel.LanguageFunctionModel>> written =
new HashMap<>();
+ for (LanguageModel.LanguageFunctionModel f :
simple.getFunctions()) {
+ String insert = SimpleCompletions.insertOf(f);
+ String key = insert.endsWith("}") ? insert.substring(0,
insert.length() - 1) : insert;
+ written.computeIfAbsent(key, k -> new ArrayList<>()).add(f);
+ if (key.endsWith("()")) {
+ // uuid() is also written uuid(short)
+ written.computeIfAbsent(key.substring(0, key.length() -
1), k -> new ArrayList<>()).add(f);
+ }
+ }
+ m = new Model(catalog, simple, written);
+ model = m;
+ }
+ return m;
+ }
+
+ private static String simpleType(String type) {
+ int dot = type.lastIndexOf('.');
+ return dot >= 0 ? type.substring(dot + 1) : type;
+ }
+
+ private static String firstSentence(String text) {
+ if (text == null) {
+ return "";
+ }
+ int dot = text.indexOf(". ");
+ return dot > 0 ? text.substring(0, dot + 1) : text;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
index 062f631d5e11..b5afd4eb7a42 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
@@ -1011,6 +1011,11 @@ final class SourceEditAssist {
return items;
}
+ /** The quick doc of the simple function at the cursor (CAMEL-25219). */
+ List<SourceViewer.DocEntry> provideSimpleQuickDoc(List<String> lines, int
row, int col) {
+ return SimpleQuickDoc.at(validationCatalog(), lines, row, col);
+ }
+
/** The completions of the simple expression at the cursor (CAMEL-25219).
*/
List<AutocompletePopup.CompletionItem>
provideSimpleCompletions(SimpleCompletionContext context, List<String> lines) {
return SimpleCompletions.provide(validationCatalog(), context, lines);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 05a641c5e5d1..9eff3188c3fe 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -848,6 +848,8 @@ class SourceTab extends AbstractTab {
// simple expressions in every route file: YAML, Java and XML
(CAMEL-25219)
sourceViewer.setSimpleCompletion(
routeFile || isCamelSourceFile(filePath) ?
assist::provideSimpleCompletions : null);
+ sourceViewer.setCursorQuickDocProvider(
+ routeFile || isCamelSourceFile(filePath) ?
assist::provideSimpleQuickDoc : null);
if (isCamelSourceFile(filePath)) {
sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs);
sourceViewer.setDeprecatedLineScanner(null);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index 229f7d4f56f3..2c2b081f8c21 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -103,6 +103,12 @@ class SourceViewer {
List<DocEntry> provideForLine(List<String> lines, int cursorRow);
}
+ /** The quick doc of what the cursor is on within the line; col is -1 in
the view, which has no cursor. */
+ @FunctionalInterface
+ interface CursorQuickDocProvider {
+ List<DocEntry> provideAt(List<String> lines, int row, int col);
+ }
+
@FunctionalInterface
interface PropertiesValidator {
String validate(String line);
@@ -217,6 +223,7 @@ class SourceViewer {
private UriCompletion pendingUriCompletion;
private BiFunction<SimpleCompletionContext, List<String>,
List<AutocompletePopup.CompletionItem>> simpleCompletion;
private SimpleCompletion pendingSimpleCompletion;
+ private CursorQuickDocProvider cursorQuickDocProvider;
private List<String> routeProblems = List.of();
private List<String> validationErrors;
private int validationErrorScroll;
@@ -423,6 +430,14 @@ class SourceViewer {
private record SimpleCompletion(int row, int endCol,
SimpleCompletionContext context) {
}
+ /**
+ * The quick doc of the simple function the cursor is on (CAMEL-25219),
shown before the doc of the line in edit
+ * mode, and the functions of the selected line after it in the view; null
for a file without routes.
+ */
+ void setCursorQuickDocProvider(CursorQuickDocProvider provider) {
+ this.cursorQuickDocProvider = provider;
+ }
+
/** Opens the AI panel to fix the problem of the cursor line (Shift+F8);
null when there is no AI panel. */
void setAskAi(MonitorContext.AskAi askAi) {
this.askAi = askAi;
@@ -442,6 +457,7 @@ class SourceViewer {
routeValidator = null;
uriCompletion = null;
simpleCompletion = null;
+ cursorQuickDocProvider = null;
}
void reset() {
@@ -484,6 +500,7 @@ class SourceViewer {
routeValidator = null;
uriCompletion = null;
simpleCompletion = null;
+ cursorQuickDocProvider = null;
}
boolean isMarkdownMode() {
@@ -2149,6 +2166,17 @@ class SourceViewer {
rawLines.add(jo.getString("code") != null ?
jo.getString("code") : "");
}
viewDocEntries = editQuickDocProvider.provideForLine(rawLines,
selectedLine);
+ if (cursorQuickDocProvider != null) {
+ List<DocEntry> used =
cursorQuickDocProvider.provideAt(rawLines, selectedLine, -1);
+ if (used != null && !used.isEmpty()) {
+ List<DocEntry> merged = new ArrayList<>();
+ if (viewDocEntries != null) {
+ merged.addAll(viewDocEntries);
+ }
+ merged.addAll(used);
+ viewDocEntries = merged;
+ }
+ }
}
}
@@ -2358,7 +2386,20 @@ class SourceViewer {
editorArea = new Rect(inner.left(), inner.top(), inner.width(),
inner.height() - docPanelHeight);
docArea = new Rect(inner.left(), inner.top() + inner.height() -
docPanelHeight, inner.width(), docPanelHeight);
lastVisibleLines = Math.max(1, editorArea.height());
- editDocEntries = editQuickDocProvider.provideForLine(editLines(),
editState.cursorRow());
+ List<String> textLines = editLines();
+ editDocEntries = editQuickDocProvider.provideForLine(textLines,
editState.cursorRow());
+ if (cursorQuickDocProvider != null) {
+ List<DocEntry> here =
cursorQuickDocProvider.provideAt(textLines, editState.cursorRow(),
+ editState.cursorCol());
+ if (here != null && !here.isEmpty()) {
+ // the function the cursor is on is more to the point than
the step of the line
+ List<DocEntry> merged = new ArrayList<>(here);
+ if (editDocEntries != null) {
+ merged.addAll(editDocEntries);
+ }
+ editDocEntries = merged;
+ }
+ }
}
int prefixWidth = plainMode ? 0 : 3;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 4e39454073f4..6169e2386e74 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -51,7 +51,8 @@ a heat map of the route: where messages go, where they fail.
- **Ctrl+K** — delete current line
- **Ctrl+Left / Ctrl+Right** — word navigation
- **Home** — smart home (content indent, then column 0)
-- Quick documentation panel is shown at the bottom (shows doc for current line)
+- Quick documentation panel is shown at the bottom (shows doc for current line;
+ in a simple expression, the function, header or operator the cursor is on)
- **F7** — show diff of unsaved changes
- **F9** — jump to next validation error
- **Shift+F8** — ask the AI to fix the problem on the cursor line: the file is
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleQuickDocTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleQuickDocTest.java
new file mode 100644
index 000000000000..c1ac71ff0717
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleQuickDocTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The quick doc of the simple function the cursor is on (CAMEL-25219).
+ */
+class SimpleQuickDocTest {
+
+ private static CamelCatalog catalog;
+
+ @BeforeAll
+ static void loadCatalog() {
+ catalog = new DefaultCamelCatalog();
+ }
+
+ @Test
+ void theFunctionTheCursorIsOn() {
+ String line = " .setBody(simple(\"Total ${abs(${header.price})}
at ${date:now}\"))";
+ int abs = line.indexOf("abs");
+ int price = line.indexOf("price");
+ int date = line.indexOf("date");
+
+ assertThat(SimpleCompletionContext.functionAt(line,
abs).text()).isEqualTo("abs(${header.price})");
+ // the innermost of nested ones
+ assertThat(SimpleCompletionContext.functionAt(line,
price).text()).isEqualTo("header.price");
+ assertThat(SimpleCompletionContext.functionAt(line,
date).text()).isEqualTo("date:now");
+ // on the ${ and on the }
+ assertThat(SimpleCompletionContext.functionAt(line, date -
2).text()).isEqualTo("date:now");
+ assertThat(SimpleCompletionContext.functionAt(line,
line.indexOf("}\"")).text()).isEqualTo("date:now");
+ // between them, and one being typed
+ assertThat(SimpleCompletionContext.functionAt(line, line.indexOf(" at
"))).isNull();
+ assertThat(SimpleCompletionContext.functionAt("\"${bod",
6).text()).isEqualTo("bod");
+ }
+
+ @Test
+ void writtenFunctionsAreFoundInTheCatalog() {
+ assertThat(name("body")).isEqualTo("body");
+ assertThat(name("body.length")).isEqualTo("body");
+ assertThat(name("header.priority")).isEqualTo("header.name");
+
assertThat(name("headerAs(foo,Integer)")).isEqualTo("headerAs(key,type)");
+ assertThat(name("date:now-24h")).isEqualTo("date(command)");
+ assertThat(name("bean:myBean.hello")).isEqualTo("bean(name.method)");
+ assertThat(name("uuid(short)")).isEqualTo("uuid(type)");
+ assertThat(name("uuid()")).isEqualTo("uuid(type)");
+ // overloads by their number of arguments
+
assertThat(name("a2a:emit('Searching')")).isEqualTo("a2a:emit(message)");
+ assertThat(name("a2a:emit(WORKING,'Searching,
still')")).isEqualTo("a2a:emit(state,message)");
+ assertThat(SimpleQuickDoc.resolve(catalog, "noSuchThing")).isNull();
+ }
+
+ private static String name(String written) {
+ return SimpleQuickDoc.resolve(catalog, written).getName();
+ }
+
+ @Test
+ void theDocOfAFunction() {
+ List<String> lines = List.of(" .setBody(simple(\"Created
${date:now-24h}\"))");
+ List<String> doc = texts(SimpleQuickDoc.at(catalog, lines, 0,
lines.get(0).indexOf("now")));
+ assertThat(doc.get(0)).startsWith("date:command — Date — ");
+ assertThat(doc).anyMatch(t -> t.startsWith("Example: ${date:"));
+ assertThat(doc).anyMatch(t -> t.startsWith("command (required)"));
+ }
+
+ @Test
+ void theDocOfAHeaderTheFileSets() {
+ List<String> lines = List.of(
+ "from(\"kafka:orders\")",
+ " .setHeader(\"priority\", constant(1))",
+ " .filter(simple(\"${header.priority} == 1 &&
${header.CamelKafkaKey} != null\"))");
+ String line = lines.get(2);
+ assertThat(texts(SimpleQuickDoc.at(catalog, lines, 2,
line.indexOf("priority"))))
+ .contains("priority — Set in this file");
+ assertThat(texts(SimpleQuickDoc.at(catalog, lines, 2,
line.indexOf("CamelKafkaKey"))))
+ .anyMatch(t -> t.startsWith("CamelKafkaKey — kafka — "));
+ }
+
+ @Test
+ void theDocOfAnOperator() {
+ List<String> lines = List.of("<simple>${body} contains
'Camel'</simple>");
+ List<String> doc = texts(SimpleQuickDoc.at(catalog, lines, 0,
lines.get(0).indexOf("tains")));
+ assertThat(doc.get(0)).startsWith("contains — Contains — ");
+ assertThat(doc).contains("Syntax: LHS contains RHS");
+
+ // a word of a log message is no operator
+ lines = List.of(" .log(\"Got contains\")");
+ assertThat(SimpleQuickDoc.at(catalog, lines, 0,
lines.get(0).indexOf("contains"))).isEmpty();
+ }
+
+ @Test
+ void theViewListsTheFunctionsOfTheLine() {
+ List<String> lines = List.of(" simple: \"${header.foo} at
${date:now:yyyyMMdd} by ${body}\"");
+ assertThat(texts(SimpleQuickDoc.at(catalog, lines, 0, -1)))
+ .containsExactly("Simple functions: header.name, date:command,
body");
+ assertThat(SimpleQuickDoc.at(catalog, List.of(" .to(\"log:x\")"),
0, -1)).isEmpty();
+ }
+
+ private static List<String> texts(List<SourceViewer.DocEntry> entries) {
+ return entries.stream().map(SourceViewer.DocEntry::text).toList();
+ }
+}