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 aa62d0158e1d chore: camel-jbang - quick fixes for the problems route 
validation reports (camel_validate_source fixes) (#27187)
aa62d0158e1d is described below

commit aa62d0158e1dc9d89efb98166578c7096f848fee
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 13:18:16 2026 +0200

    chore: camel-jbang - quick fixes for the problems route validation reports 
(camel_validate_source fixes) (#27187)
    
    Quick fixes of the problems the route validation reports (option typos, 
enum values, to that should be toD, a Simple expression where a placeholder is 
meant): Shift+F9 in the source editor, and as fixes in camel_validate_source.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |   5 +
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  31 ++++
 .../dsl/jbang/core/commands/ai/QuickFixes.java     | 198 +++++++++++++++++++++
 .../dsl/jbang/core/commands/ai/QuickFixesTest.java | 153 ++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  52 ++++++
 .../src/main/resources/tui/help/source.md          |   3 +
 .../core/commands/tui/SourceQuickFixTest.java      |  90 ++++++++++
 7 files changed, 532 insertions(+)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index cfe0839128e2..e8fcc5faf483 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -265,6 +265,11 @@ and the problem of the selected line in the panel at the 
bottom.
 An XML file with problems is not saved, as a YAML file; a Java file is saved 
and its problems are said, as it is
 the application's code, and a check that cannot know something (a value 
computed at runtime) must not stop you.
 
+When the problem on the line of the cursor says its fix (an option that is a 
typo of another, an enum value a letter
+off, a `to` that should be a `toD`, a Simple expression `+${key}+` where the 
property placeholder `+{{key}}+` is meant),
+the Error panel shows it and *Shift+F9* applies it; *Ctrl+Z* takes it back. 
`camel_validate_source` gives the same fixes to
+an agent, as `fixes` with the text to find and its replacement.
+
 Validation can be toggled off in the Settings (F2 > Settings).
 
 == Activity
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 298c1d615a4b..bcbccac9a3b0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -498,6 +498,24 @@ public final class AuthoringTools {
         result.put("valid", errors.isEmpty());
         result.put("file", file);
         result.put("errors", new JsonArray(errors));
+        // the problems whose fix is certain, as edits an agent can apply 
(camel_edit_file find/replace)
+        JsonArray fixes = new JsonArray();
+        String[] lines = content.split("\n", -1);
+        for (String error : errors) {
+            int line = lineOf(error);
+            QuickFixes.Fix fix = line > 0 && line <= lines.length ? 
QuickFixes.fixFor(error, lines[line - 1]) : null;
+            if (fix != null) {
+                JsonObject jo = new JsonObject();
+                jo.put("line", line);
+                jo.put("find", fix.oldText());
+                jo.put("replace", fix.newText());
+                jo.put("fix", fix.label());
+                fixes.add(jo);
+            }
+        }
+        if (!fixes.isEmpty()) {
+            result.put("fixes", fixes);
+        }
         if (RouteAssist.supports(file, content)) {
             // the parts of a Java or XML route the parser could not read, so 
the checks did not see (CAMEL-25208)
             JsonArray notChecked = new JsonArray();
@@ -517,6 +535,19 @@ public final class AuthoringTools {
         return result;
     }
 
+    /** The line of a validation message, "Line N: ...", or 0. */
+    private static int lineOf(String error) {
+        if (error == null || !error.startsWith("Line ")) {
+            return 0;
+        }
+        int colon = error.indexOf(':');
+        try {
+            return colon > 5 ? Integer.parseInt(error.substring(5, 
colon).trim()) : 0;
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
+
     /** How long a write waits for the running integration's reload record 
before answering without it. */
     static final long RELOAD_WAIT_MILLIS = 8000;
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
new file mode 100644
index 000000000000..b8065931d337
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
@@ -0,0 +1,198 @@
+/*
+ * 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.ai;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The fix of a problem the validation reports, when the problem says it: an 
option that is a typo of another (Did you
+ * mean: [period]), an enum value of the wrong case, a to that needs toD, a 
Simple expression where a property
+ * placeholder is meant. The fix is a replacement of text on the line of the 
problem, which an editor applies with one
+ * key and an agent applies as an edit, whatever the DSL (YAML, Java, XML) the 
line is written in.
+ * <p/>
+ * A fix is only given when it is certain: one suggestion, a value that 
matches but for its case or a letter or two.
+ */
+public final class QuickFixes {
+
+    /**
+     * A replacement on the line of a problem.
+     *
+     * @param label   what the fix does, such as "peroid → period"
+     * @param oldText the text on the line to replace (its first occurrence)
+     * @param newText what it is replaced with
+     */
+    public record Fix(String label, String oldText, String newText) {
+
+        /** The line with the fix applied, or null when the line does not have 
the text to replace. */
+        public String apply(String line) {
+            int i = line != null ? line.indexOf(oldText) : -1;
+            return i < 0 ? null : line.substring(0, i) + newText + 
line.substring(i + oldText.length());
+        }
+    }
+
+    private static final Pattern UNKNOWN_OPTION
+            = Pattern.compile("Unknown option '([^']+)'\\. Did you mean: 
\\[([^\\],]+)\\]");
+    private static final Pattern UNKNOWN_PROPERTY
+            = Pattern.compile("property '([^']+)' is not defined in the 
schema.*did you mean '([^']+)'\\?");
+    private static final Pattern INVALID_ENUM
+            = Pattern.compile("Invalid enum value '([^']*)' for option 
'([^']+)'\\. Possible values: \\[([^\\]]*)\\]");
+    private static final Pattern INVALID_BOOLEAN = Pattern.compile("Invalid 
boolean value '([^']*)' for option '([^']+)'");
+    private static final Pattern SIMPLE_AS_PLACEHOLDER = Pattern.compile(
+            "([\\w.-]+)=(\\$\\{[^}]*\\}) is a Simple expression, which an 
endpoint option is not evaluated as.*\\{\\{([^}]+)\\}\\}");
+    private static final Pattern DYNAMIC_TO = Pattern.compile("holds an 
expression \\(\\$\\{");
+
+    private QuickFixes() {
+    }
+
+    /**
+     * The fix of a problem.
+     *
+     * @param  message the message of the problem, with or without its "Line 
N: " in front
+     * @param  line    the text of the line the problem is on
+     * @return         the fix, or null when the problem has none or the line 
does not have what it names
+     */
+    public static Fix fixFor(String message, String line) {
+        if (message == null || line == null) {
+            return null;
+        }
+        Fix fix = find(message, line);
+        return fix != null && fix.apply(line) != null ? fix : null;
+    }
+
+    private static Fix find(String message, String line) {
+        Matcher m = UNKNOWN_OPTION.matcher(message);
+        if (m.find()) {
+            return rename(line, m.group(1), m.group(2).trim());
+        }
+        m = UNKNOWN_PROPERTY.matcher(message);
+        if (m.find()) {
+            return rename(line, m.group(1), m.group(2));
+        }
+        m = INVALID_ENUM.matcher(message);
+        if (m.find()) {
+            List<String> choices = new ArrayList<>();
+            for (String c : m.group(3).split(",")) {
+                if (!c.isBlank()) {
+                    choices.add(c.trim());
+                }
+            }
+            return value(line, m.group(2), m.group(1), closest(m.group(1), 
choices));
+        }
+        m = INVALID_BOOLEAN.matcher(message);
+        if (m.find()) {
+            return value(line, m.group(2), m.group(1), closest(m.group(1), 
List.of("true", "false")));
+        }
+        m = SIMPLE_AS_PLACEHOLDER.matcher(message);
+        if (m.find()) {
+            String placeholder = "{{" + m.group(3) + "}}";
+            return new Fix(m.group(2) + " → " + placeholder, m.group(2), 
placeholder);
+        }
+        if (DYNAMIC_TO.matcher(message).find() && message.contains("toD")) {
+            // the step itself: .to( in Java, <to in XML, to: in YAML
+            for (String[] form : new String[][] { { ".to(", ".toD(" }, { "<to 
", "<toD " }, { "to:", "toD:" } }) {
+                int i = line.indexOf(form[0]);
+                if (i >= 0 && (form[0].startsWith(".") || 
form[0].startsWith("<") || isYamlKeyAt(line, i))) {
+                    return new Fix("to → toD", form[0], form[1]);
+                }
+            }
+        }
+        return null;
+    }
+
+    /** An option written name= in a uri or name: as a YAML key, renamed. */
+    private static Fix rename(String line, String from, String to) {
+        if (line.contains(from + "=")) {
+            return new Fix(from + " → " + to, from + "=", to + "=");
+        }
+        int i = line.indexOf(from + ":");
+        if (i >= 0 && isYamlKeyAt(line, i)) {
+            return new Fix(from + " → " + to, from + ":", to + ":");
+        }
+        return null;
+    }
+
+    /** The value of an option, written name=value in a uri or name: value as 
a YAML key, replaced. */
+    private static Fix value(String line, String option, String from, String 
to) {
+        if (to == null || to.equals(from)) {
+            return null;
+        }
+        for (String sep : new String[] { "=", ": ", ":" }) {
+            String old = option + sep + from;
+            if (line.contains(old)) {
+                return new Fix(from + " → " + to, old, option + sep + to);
+            }
+        }
+        return null;
+    }
+
+    /** Whether the text at the index starts a YAML key: at the start of the 
line, after its indent or a "- ". */
+    private static boolean isYamlKeyAt(String line, int i) {
+        String before = line.substring(0, i).strip();
+        return before.isEmpty() || before.equals("-");
+    }
+
+    /**
+     * The choice the value is meant to be: the one equal but for its case, 
else the one a letter or two away when only
+     * one is; null when it is not clear.
+     */
+    static String closest(String value, List<String> choices) {
+        for (String c : choices) {
+            if (c.equalsIgnoreCase(value)) {
+                return c;
+            }
+        }
+        String best = null;
+        int bestDistance = Integer.MAX_VALUE;
+        boolean tie = false;
+        for (String c : choices) {
+            int d = distance(value.toLowerCase(Locale.ROOT), 
c.toLowerCase(Locale.ROOT));
+            if (d < bestDistance) {
+                best = c;
+                bestDistance = d;
+                tie = false;
+            } else if (d == bestDistance) {
+                tie = true;
+            }
+        }
+        int allowed = Math.max(1, Math.min(2, value.length() / 3));
+        return best != null && !tie && bestDistance <= allowed ? best : null;
+    }
+
+    /** The Levenshtein distance of two words. */
+    private static int distance(String a, String b) {
+        int[] prev = new int[b.length() + 1];
+        int[] cur = new int[b.length() + 1];
+        for (int j = 0; j <= b.length(); j++) {
+            prev[j] = j;
+        }
+        for (int i = 1; i <= a.length(); i++) {
+            cur[0] = i;
+            for (int j = 1; j <= b.length(); j++) {
+                int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
+                cur[j] = Math.min(Math.min(cur[j - 1] + 1, prev[j] + 1), 
prev[j - 1] + cost);
+            }
+            int[] t = prev;
+            prev = cur;
+            cur = t;
+        }
+        return prev[b.length()];
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixesTest.java
new file mode 100644
index 000000000000..7c73c78e2213
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixesTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.ai;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The fixes of the problems the validation reports, applied to their line, in 
the three DSLs.
+ */
+class QuickFixesTest {
+
+    private static CamelCatalog catalog;
+
+    @BeforeAll
+    static void loadCatalog() {
+        catalog = new DefaultCamelCatalog();
+    }
+
+    /** The line of the only problem of the source, with its fix applied. */
+    private static String fixed(String file, String content) {
+        List<String> errors = SourceValidator.validate(file, content, catalog, 
null, null);
+        assertThat(errors).as(file).hasSize(1);
+        String error = errors.get(0);
+        int line = Integer.parseInt(error.substring(5, error.indexOf(':')));
+        String text = content.split("\n", -1)[line - 1];
+        QuickFixes.Fix fix = QuickFixes.fixFor(error, text);
+        assertThat(fix).as(error).isNotNull();
+        return fix.apply(text).strip();
+    }
+
+    private static String java(String steps) {
+        return """
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class MyRoute extends RouteBuilder {
+                    @Override
+                    public void configure() throws Exception {
+                %s
+                    }
+                }
+                """.formatted(steps);
+    }
+
+    @Test
+    void aTypoOfAnOption() {
+        assertThat(fixed("MyRoute.java", java("        
from(\"timer:tick?peroid=1000\").to(\"seda:out\");")))
+                
.isEqualTo("from(\"timer:tick?period=1000\").to(\"seda:out\");");
+        assertThat(fixed("routes.xml", """
+                <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                    <route>
+                        <from uri="timer:tick?peroid=1000"/>
+                        <to uri="seda:out"/>
+                    </route>
+                </routes>
+                """)).isEqualTo("<from uri=\"timer:tick?period=1000\"/>");
+    }
+
+    @Test
+    void aTypoOfAYamlOption() {
+        assertThat(fixed("route.camel.yaml", """
+                - route:
+                    from:
+                      uri: "timer:tick"
+                      steps:
+                        - log:
+                            message: "${body}"
+                            logLevel: WARN
+                """)).isEqualTo("loggingLevel: WARN");
+    }
+
+    @Test
+    void anEnumValueALetterOff() {
+        assertThat(fixed("MyRoute.java", java("        
from(\"timer:tick\").to(\"file:out?fileExist=Overide\");")))
+                
.isEqualTo("from(\"timer:tick\").to(\"file:out?fileExist=Override\");");
+    }
+
+    @Test
+    void aBooleanValue() {
+        assertThat(fixed("MyRoute.java", java("        
from(\"timer:tick\").to(\"log:x?showAll=tru\");")))
+                .isEqualTo("from(\"timer:tick\").to(\"log:x?showAll=true\");");
+    }
+
+    @Test
+    void aToThatNeedsToD() {
+        assertThat(fixed("MyRoute.java", java("        from(\"timer:tick\")\n  
          .to(\"seda:${header.q}\");")))
+                .isEqualTo(".toD(\"seda:${header.q}\");");
+        assertThat(fixed("routes.xml", """
+                <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                    <route>
+                        <from uri="timer:tick"/>
+                        <to uri="seda:${header.q}"/>
+                    </route>
+                </routes>
+                """)).isEqualTo("<toD uri=\"seda:${header.q}\"/>");
+    }
+
+    @Test
+    void aSimpleExpressionWhereAPlaceholderIsMeant() {
+        assertThat(fixed("MyRoute.java", java("        
from(\"timer:tick?period=${welcome.period}\").to(\"seda:out\");")))
+                
.isEqualTo("from(\"timer:tick?period={{welcome.period}}\").to(\"seda:out\");");
+    }
+
+    @Test
+    void theValidateToolGivesTheFixesAsEdits() throws Exception {
+        Map<String, String> args = new HashMap<>();
+        args.put("file", "MyRoute.java");
+        args.put("content", java("        
from(\"timer:tick?peroid=1000\").to(\"seda:out\");"));
+        JsonObject result = (JsonObject) Jsoner
+                
.deserialize(String.valueOf(ToolRegistry.execute("camel_validate_source", new 
ToolContext(), args)));
+        JsonArray fixes = (JsonArray) result.get("fixes");
+        assertThat(fixes).hasSize(1);
+        JsonObject fix = (JsonObject) fixes.get(0);
+        assertThat(fix.getInteger("line")).isEqualTo(6);
+        assertThat(fix.getString("find")).isEqualTo("peroid=");
+        assertThat(fix.getString("replace")).isEqualTo("period=");
+    }
+
+    @Test
+    void noFixWhenItIsNotCertain() {
+        // two choices as close, a value nothing like any, a line without the 
text
+        assertThat(QuickFixes.closest("ab", List.of("ac", "ad"))).isNull();
+        assertThat(QuickFixes.closest("Bogus", List.of("Override", "Append", 
"Fail"))).isNull();
+        assertThat(QuickFixes.fixFor("timer: Unknown option 'peroid'. Did you 
mean: [period]", "nothing here"))
+                .isNull();
+        assertThat(QuickFixes.fixFor("Simple syntax error: Unexpected token", 
"simple(\"x\")")).isNull();
+    }
+}
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 bea53fe8a16e..0295c4e6739c 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
@@ -57,6 +57,7 @@ import dev.tamboui.widgets.input.TextAreaState;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import dev.tamboui.widgets.scrollbar.Scrollbar;
 import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import org.apache.camel.dsl.jbang.core.commands.ai.QuickFixes;
 import org.apache.camel.support.LoggerHelper;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.json.JsonArray;
@@ -869,6 +870,11 @@ class SourceViewer {
             }
             return true;
         }
+        if (ke.isKey(KeyCode.F9) && ke.hasShift()) {
+            // Shift+F9 fixes the problem F9 jumps to (F3, F6, F8 are global: 
switch integration, shell, AI panel)
+            applyQuickFix();
+            return true;
+        }
         if (ke.isKey(KeyCode.F9) && !inlineErrors.isEmpty()) {
             jumpToNextError();
             return true;
@@ -1643,6 +1649,43 @@ class SourceViewer {
         }
     }
 
+    /** The fix of the problem on the line of the cursor, when the problem 
says it; null otherwise. */
+    private QuickFixes.Fix cursorFix() {
+        int row = editState.cursorRow();
+        String error = visibleInlineErrors().get(row);
+        return error != null ? QuickFixes.fixFor(error, 
editState.getLine(row)) : null;
+    }
+
+    /** Applies the fix of the problem on the line of the cursor (Shift+F9), 
and checks the content again right away. */
+    private void applyQuickFix() {
+        QuickFixes.Fix fix = cursorFix();
+        if (fix == null) {
+            return;
+        }
+        int row = editState.cursorRow();
+        int col = editState.cursorCol();
+        String line = editState.getLine(row);
+        String fixed = fix.apply(line);
+        if (fixed == null) {
+            return;
+        }
+        recordEditChange();
+        editState.moveCursorToLineStart();
+        for (int i = 0; i < line.length(); i++) {
+            editState.deleteForward();
+        }
+        editState.insert(fixed);
+        editState.moveCursorToLineStart();
+        for (int i = 0; i < Math.min(col, fixed.length()); i++) {
+            editState.moveCursorRight();
+        }
+        dirty = true;
+        lineStatuses = null;
+        // the problem is gone: check again now, not at the next interval
+        lastBackgroundValidationTime = 0;
+        notifySave("Fixed: " + fix.label(), false);
+    }
+
     private void jumpToNextError() {
         goToNextProblem(inlineErrors, editState.cursorRow());
     }
@@ -2269,6 +2312,12 @@ class SourceViewer {
                         Span.styled(titleText, errorDim.bold()),
                         Span.styled(suffix + "─".repeat(remaining), 
errorDim)));
                 docLines.add(Line.from(Span.styled(cursorError, errorDim)));
+                QuickFixes.Fix fix = cursorFix();
+                if (fix != null) {
+                    docLines.add(Line.from(
+                            Span.styled("Shift+F9", Style.EMPTY.bold()),
+                            Span.styled(" fix: " + fix.label(), 
Style.EMPTY.dim())));
+                }
             } else if (titleText != null) {
                 String prefix = "─── ";
                 String suffix = " ";
@@ -2468,6 +2517,9 @@ class SourceViewer {
             if (!inlineErrors.isEmpty()) {
                 TuiHelper.hint(spans, "F9", "next error");
             }
+            if (cursorFix() != null) {
+                TuiHelper.hint(spans, "Shift+F9", "fix");
+            }
             if (isCamelYamlFile()) {
                 TuiHelper.hint(spans, "Ctrl+R", "refactor");
             }
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 7cb73d095bf7..2b3834edded9 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
@@ -54,6 +54,9 @@ a heat map of the route: where messages go, where they fail.
 - Quick documentation panel is shown at the bottom (shows doc for current line)
 - **F7** — show diff of unsaved changes
 - **F9** — jump to next validation error
+- **Shift+F9** — apply the fix of the problem on the cursor line, when the 
problem
+  says it (an option typo, an enum value a letter off, `to` that should be
+  `toD`, `${key}` where `{{key}}` is meant); the Error panel shows the fix
 - Java and XML DSL routes are checked as you type, like YAML routes: endpoint
   options, simple expressions, and a `to` with `${...}` that should be a `toD`.
   The problems are marked on their lines; an XML file with problems is not 
saved
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceQuickFixTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceQuickFixTest.java
new file mode 100644
index 000000000000..f47d9217eada
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceQuickFixTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Shift+F9 in the Source editor applies the fix of the problem on the line of 
the cursor.
+ */
+class SourceQuickFixTest {
+
+    @TempDir
+    Path tempDir;
+
+    private final AtomicReference<String> lastNotification = new 
AtomicReference<>();
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+    }
+
+    @Test
+    void shiftF9AppliesTheFixOfTheProblemOnTheLine() throws Exception {
+        Path file = tempDir.resolve("MyRoute.java");
+        Files.writeString(file, """
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class MyRoute extends RouteBuilder {
+                    @Override
+                    public void configure() throws Exception {
+                        from("timer:tick?peroid=1000")
+                            .to("seda:out");
+                    }
+                }
+                """, StandardCharsets.UTF_8);
+        AtomicReference<List<IntegrationInfo>> data = new 
AtomicReference<>(List.of());
+        AtomicReference<List<InfraInfo>> infraData = new 
AtomicReference<>(List.of());
+        SourceEditAssist assist = new SourceEditAssist(new 
MonitorContext(data, infraData));
+        SourceViewer viewer = new SourceViewer();
+        viewer.setNotificationCallback((msg, error) -> 
lastNotification.set(msg));
+        viewer.setRouteValidator(content -> assist.validateRoutes(file, 
content));
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+        // a Java file with a problem is saved, and the problem marked on its 
line
+        viewer.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.CTRL));
+        assertThat(viewer.inlineErrors()).containsOnlyKeys(5);
+
+        // Shift+F9 elsewhere does nothing
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F9, KeyModifiers.SHIFT));
+        assertThat(viewer.editText()).contains("peroid=1000");
+
+        for (int i = 0; i < 5; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F9, KeyModifiers.SHIFT));
+        assertThat(viewer.editText().split("\n")[5]).isEqualTo("        
from(\"timer:tick?period=1000\")");
+        assertThat(lastNotification.get()).isEqualTo("Fixed: peroid → period");
+
+        // undo takes the fix back
+        viewer.handleKeyEvent(KeyEvent.ofChar('z', KeyModifiers.CTRL));
+        assertThat(viewer.editText()).contains("peroid=1000");
+    }
+}

Reply via email to