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();
+    }
+}

Reply via email to