diff --git a/pom.xml b/pom.xml
index ca34f79ec..09e826bca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,9 +63,9 @@
4.3.0
2.25.2
-
- 2.4.240
- 1.5.21
+
+ 2.4.240
+ 1.5.21
1.0.0-alpha.5
diff --git a/spring-shell-core-autoconfigure/src/main/java/org/springframework/shell/core/autoconfigure/JLineShellAutoConfiguration.java b/spring-shell-core-autoconfigure/src/main/java/org/springframework/shell/core/autoconfigure/JLineShellAutoConfiguration.java
index a59f7e460..24502b161 100644
--- a/spring-shell-core-autoconfigure/src/main/java/org/springframework/shell/core/autoconfigure/JLineShellAutoConfiguration.java
+++ b/spring-shell-core-autoconfigure/src/main/java/org/springframework/shell/core/autoconfigure/JLineShellAutoConfiguration.java
@@ -19,11 +19,9 @@
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-import org.jline.reader.Highlighter;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.Parser;
@@ -32,7 +30,6 @@
import org.jline.terminal.TerminalBuilder;
import org.jline.terminal.TerminalBuilder.SystemOutput;
import org.jline.utils.AttributedString;
-import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.BeanCreationException;
@@ -50,6 +47,7 @@
import org.springframework.shell.core.command.CommandRegistry;
import org.springframework.shell.core.config.UserConfigPathProvider;
import org.springframework.shell.jline.CommandCompleter;
+import org.springframework.shell.jline.CommandHighlighter;
import org.springframework.shell.jline.ExtendedDefaultParser;
import org.springframework.shell.jline.JLineInputProvider;
import org.springframework.shell.jline.PromptProvider;
@@ -62,6 +60,7 @@
* @author Eric Bottard
* @author Florent Biville
* @author Mahmoud Ben Hassine
+ * @author Piotr Olaszewski
*/
@AutoConfiguration
@EnableConfigurationProperties(SpringShellProperties.class)
@@ -98,36 +97,7 @@ public LineReader lineReader(Terminal terminal, Parser parser, CommandCompleter
.appName("Spring Shell")
.completer(commandCompleter)
.history(jLineHistory)
- .highlighter(new Highlighter() {
-
- @Override
- public AttributedString highlight(LineReader reader, String buffer) {
- int l = 0;
- String best = null;
- for (Command command : commandRegistry.getCommands()) {
- if (buffer.startsWith(command.getName()) && command.getName().length() > l) {
- l = command.getName().length();
- best = command.getName();
- }
- }
- if (best != null) {
- return new AttributedStringBuilder(buffer.length()).append(best, AttributedStyle.BOLD)
- .append(buffer.substring(l))
- .toAttributedString();
- }
- else {
- return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
- }
- }
-
- @Override
- public void setErrorPattern(Pattern errorPattern) {
- }
-
- @Override
- public void setErrorIndex(int errorIndex) {
- }
- })
+ .highlighter(new CommandHighlighter(commandRegistry))
.parser(parser);
LineReader lineReader = lineReaderBuilder.build();
diff --git a/spring-shell-jline/src/main/java/org/springframework/shell/jline/CommandHighlighter.java b/spring-shell-jline/src/main/java/org/springframework/shell/jline/CommandHighlighter.java
new file mode 100644
index 000000000..d50a8b87a
--- /dev/null
+++ b/spring-shell-jline/src/main/java/org/springframework/shell/jline/CommandHighlighter.java
@@ -0,0 +1,39 @@
+package org.springframework.shell.jline;
+
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+import org.jline.reader.Highlighter;
+import org.jline.reader.LineReader;
+import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
+
+import org.springframework.shell.core.command.CommandRegistry;
+
+/**
+ * @author Piotr Olaszewski
+ * @since 4.0.1
+ */
+public class CommandHighlighter implements Highlighter {
+
+ private final CommandRegistry commandRegistry;
+
+ public CommandHighlighter(CommandRegistry commandRegistry) {
+ this.commandRegistry = commandRegistry;
+ }
+
+ @Override
+ public AttributedString highlight(LineReader reader, String buffer) {
+ return commandRegistry.getCommands()
+ .stream()
+ .flatMap(command -> Stream.concat(Stream.of(command.getName()), command.getAliases().stream()))
+ .filter(buffer::startsWith)
+ .max(Comparator.comparingInt(String::length))
+ .map(bestMatch -> new AttributedStringBuilder(buffer.length()).append(bestMatch, AttributedStyle.BOLD)
+ .append(buffer.substring(bestMatch.length()))
+ .toAttributedString())
+ .orElseGet(() -> new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
+ }
+
+}
diff --git a/spring-shell-jline/src/main/java/org/springframework/shell/jline/command/package-info.java b/spring-shell-jline/src/main/java/org/springframework/shell/jline/command/package-info.java
new file mode 100644
index 000000000..df22f9880
--- /dev/null
+++ b/spring-shell-jline/src/main/java/org/springframework/shell/jline/command/package-info.java
@@ -0,0 +1,4 @@
+@NullMarked
+package org.springframework.shell.jline.command;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandCompleterTests.java b/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandCompleterTests.java
index c1983643b..6ddf9652e 100644
--- a/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandCompleterTests.java
+++ b/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandCompleterTests.java
@@ -72,7 +72,7 @@ else if ("last".equals(option.longName()) || 'l' == option.shortName()) {
};
@BeforeEach
- public void before() {
+ void before() {
command = mock(Command.class);
when(command.getName()).thenReturn("hello");
when(command.getCompletionProvider()).thenReturn(completionProvider);
@@ -86,7 +86,7 @@ private List toCandidateNames(List candidates) {
@ParameterizedTest
@MethodSource("completeData")
- public void testComplete(List words, List expectedValues) {
+ void testComplete(List words, List expectedValues) {
// given
when(command.getName()).thenReturn("hello");
when(command.getOptions())
@@ -173,7 +173,7 @@ static Stream completeData() {
@ParameterizedTest
@MethodSource("completeCommandWithLongNamesData")
- public void testCompleteCommandWithLongNames(List words, List expectedValues) {
+ void testCompleteCommandWithLongNames(List words, List expectedValues) {
// given
when(command.getOptions()).thenReturn(List.of(new CommandOption.Builder().longName("first").build(),
new CommandOption.Builder().longName("last").build()));
@@ -227,7 +227,7 @@ static Stream completeCommandWithLongNamesData() {
@ParameterizedTest
@MethodSource("completeCommandWithShortNamesData")
- public void testCompleteCommandWithShortNames(List words, List expectedValues) {
+ void testCompleteCommandWithShortNames(List words, List expectedValues) {
// given
when(command.getOptions()).thenReturn(List.of(new CommandOption.Builder().shortName('f').build(),
new CommandOption.Builder().shortName('l').build()));
@@ -279,7 +279,7 @@ static Stream completeCommandWithShortNamesData() {
@ParameterizedTest
@MethodSource("completeWithSubCommandsData")
- public void testCompleteWithSubCommands(List words, List expectedValues) {
+ void testCompleteWithSubCommands(List words, List expectedValues) {
// given
when(command.getName()).thenReturn("hello world");
when(command.getOptions())
@@ -328,7 +328,7 @@ static Stream completeWithSubCommandsData() {
@ParameterizedTest
@MethodSource("completeWithTwoOptionsWhereOneIsSubsetOfOtherData")
- public void testCompleteWithTwoOptionsWhereOneIsSubsetOfOther(List words, List expectedValues) {
+ void testCompleteWithTwoOptionsWhereOneIsSubsetOfOther(List words, List expectedValues) {
// given
when(command.getOptions()).thenReturn(List.of(new CommandOption.Builder().longName("first").build(),
new CommandOption.Builder().longName("firstname").build()));
@@ -373,7 +373,7 @@ static Stream completeWithTwoOptionsWhereOneIsSubsetOfOtherData() {
@ParameterizedTest
@MethodSource("completeWithHiddenCommandsData")
- public void testCompleteWithHiddenCommands(List words, List expectedValues) {
+ void testCompleteWithHiddenCommands(List words, List expectedValues) {
// given
when(command.getName()).thenReturn("hello visible");
when(command.getOptions()).thenReturn(List.of());
diff --git a/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandHighlighterTest.java b/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandHighlighterTest.java
new file mode 100644
index 000000000..3aff73801
--- /dev/null
+++ b/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandHighlighterTest.java
@@ -0,0 +1,68 @@
+package org.springframework.shell.jline;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.jline.reader.LineReader;
+import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStyle;
+import org.jline.utils.Colors;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.shell.core.command.Command;
+import org.springframework.shell.core.command.CommandRegistry;
+import org.springframework.shell.jline.tui.style.ThemeResolver;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Piotr Olaszewski
+ */
+class CommandHighlighterTest {
+
+ private final LineReader lineReader = mock(LineReader.class);
+
+ private final CommandRegistry commandRegistry = new CommandRegistry();
+
+ private final CommandHighlighter highlighter = new CommandHighlighter(commandRegistry);
+
+ @Test
+ void shouldHighlightCommandNameInBold() {
+ Command command = mock(Command.class);
+ when(command.getName()).thenReturn("help");
+ when(command.getAliases()).thenReturn(Collections.emptyList());
+
+ commandRegistry.registerCommand(command);
+
+ AttributedString result = highlighter.highlight(lineReader, "help argument");
+
+ assertThat(result).hasToString("help argument");
+ assertThat(result.styleAt(0).getStyle()).isEqualTo(AttributedStyle.BOLD.getStyle());
+ assertThat(result.styleAt(5).getStyle()).isNotEqualTo(AttributedStyle.BOLD.getStyle());
+ }
+
+ @Test
+ void shouldHighlightAliasInBold() {
+ Command command = mock(Command.class);
+ when(command.getName()).thenReturn("help");
+ when(command.getAliases()).thenReturn(List.of("h"));
+
+ commandRegistry.registerCommand(command);
+
+ AttributedString result = highlighter.highlight(lineReader, "h argument");
+
+ assertThat(result).hasToString("h argument");
+ assertThat(result.styleAt(0).getStyle()).isEqualTo(AttributedStyle.BOLD.getStyle());
+ assertThat(result.styleAt(2).getStyle()).isNotEqualTo(AttributedStyle.BOLD.getStyle());
+ }
+
+ @Test
+ void shouldHighlightRedWhenNoMatchFound() {
+ AttributedString result = highlighter.highlight(lineReader, "unknown");
+
+ assertThat(result).hasToString("unknown");
+ assertThat(ThemeResolver.resolveValues(result.styleAt(0)).foreground())
+ .isEqualTo(Colors.DEFAULT_COLORS_256[AttributedStyle.RED]);
+ }
+
+}