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