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