diff --git a/bundles/org.openhab.core.io.console.karaf/src/main/java/org/openhab/core/io/console/karaf/internal/CommandWrapper.java b/bundles/org.openhab.core.io.console.karaf/src/main/java/org/openhab/core/io/console/karaf/internal/CommandWrapper.java index 1d9c8315d1..5fb84dcd4a 100644 --- a/bundles/org.openhab.core.io.console.karaf/src/main/java/org/openhab/core/io/console/karaf/internal/CommandWrapper.java +++ b/bundles/org.openhab.core.io.console.karaf/src/main/java/org/openhab/core/io/console/karaf/internal/CommandWrapper.java @@ -82,8 +82,8 @@ public class CommandWrapper implements Command, Action { } @Override - public Completer getCompleter(boolean arg0) { - return null; + public Completer getCompleter(boolean scoped) { + return new CompleterWrapper(command, scoped); } @Override diff --git a/bundles/org.openhab.core.io.console.karaf/src/main/java/org/openhab/core/io/console/karaf/internal/CompleterWrapper.java b/bundles/org.openhab.core.io.console.karaf/src/main/java/org/openhab/core/io/console/karaf/internal/CompleterWrapper.java new file mode 100644 index 0000000000..0bff5f6f2a --- /dev/null +++ b/bundles/org.openhab.core.io.console.karaf/src/main/java/org/openhab/core/io/console/karaf/internal/CompleterWrapper.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.console.karaf.internal; + +import java.util.Arrays; +import java.util.List; + +import org.apache.karaf.shell.api.console.Candidate; +import org.apache.karaf.shell.api.console.CommandLine; +import org.apache.karaf.shell.api.console.Completer; +import org.apache.karaf.shell.api.console.Session; +import org.apache.karaf.shell.support.completers.StringsCompleter; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.io.console.ConsoleCommandCompleter; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; + +/** + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault({}) +public class CompleterWrapper implements Completer { + + private final @Nullable ConsoleCommandCompleter completer; + private final String command; + private final String commandDescription; + private final @Nullable String globalCommand; + + public CompleterWrapper(final ConsoleCommandExtension command, boolean scoped) { + this.completer = command.getCompleter(); + this.command = command.getCommand(); + this.commandDescription = command.getDescription(); + if (!scoped) { + globalCommand = CommandWrapper.SCOPE + ":" + this.command; + } else { + globalCommand = null; + } + } + + @Override + public int complete(Session session, CommandLine commandLine, List candidates) { + String localGlobalCommand = globalCommand; + if (commandLine.getCursorArgumentIndex() == 0) { + StringsCompleter stringsCompleter = new StringsCompleter(); + stringsCompleter.getStrings().add(command); + if (localGlobalCommand != null) { + stringsCompleter.getStrings().add(localGlobalCommand); + } + return stringsCompleter.complete(session, commandLine, candidates); + } + + if (commandLine.getArguments().length > 1) { + String arg = commandLine.getArguments()[0]; + if (!arg.equals(command) && !arg.equals(localGlobalCommand)) + return -1; + } + + if (commandLine.getCursorArgumentIndex() < 0) + return -1; + + var localCompleter = completer; + if (localCompleter == null) + return -1; + + String[] args = commandLine.getArguments(); + boolean result = localCompleter.complete(Arrays.copyOfRange(args, 1, args.length), + commandLine.getCursorArgumentIndex() - 1, commandLine.getArgumentPosition(), candidates); + return result ? commandLine.getBufferPosition() - commandLine.getArgumentPosition() : -1; + } + + // Override this method to give command descriptions if completing the command name + @Override + public void completeCandidates(Session session, CommandLine commandLine, List candidates) { + if (commandLine.getCursorArgumentIndex() == 0) { + String arg = commandLine.getArguments()[0]; + arg = arg.substring(0, commandLine.getArgumentPosition()); + + if (command.startsWith(arg)) + candidates.add(new Candidate(command, command, null, commandDescription, null, null, true)); + String localGlobalCommand = globalCommand; + if (localGlobalCommand != null && localGlobalCommand.startsWith(arg)) + candidates.add(new Candidate(localGlobalCommand, localGlobalCommand, null, commandDescription, null, + null, true)); + + return; + } + + org.apache.karaf.shell.api.console.Completer.super.completeCandidates(session, commandLine, candidates); + } +} diff --git a/bundles/org.openhab.core.io.console.karaf/src/test/java/org/openhab/core/io/console/karaf/internal/CompleterWrapperTest.java b/bundles/org.openhab.core.io.console.karaf/src/test/java/org/openhab/core/io/console/karaf/internal/CompleterWrapperTest.java new file mode 100644 index 0000000000..7513dda4a2 --- /dev/null +++ b/bundles/org.openhab.core.io.console.karaf/src/test/java/org/openhab/core/io/console/karaf/internal/CompleterWrapperTest.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.console.karaf.internal; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; + +import org.apache.karaf.shell.api.console.Candidate; +import org.apache.karaf.shell.api.console.CommandLine; +import org.apache.karaf.shell.api.console.Completer; +import org.apache.karaf.shell.api.console.Session; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.io.console.ConsoleCommandCompleter; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; + +/** + * @author Cody Cutrer - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class CompleterWrapperTest { + private @Mock @NonNullByDefault({}) ConsoleCommandExtension commandExtension; + private @Mock @NonNullByDefault({}) ConsoleCommandCompleter completer; + private @Mock @NonNullByDefault({}) Session session; + private @Mock @NonNullByDefault({}) CommandLine commandLine; + private @NonNullByDefault({}) CommandWrapper commandWrapper; + private @NonNullByDefault({}) Completer completerWrapper; + + @BeforeEach + public void setup() { + when(commandExtension.getCommand()).thenReturn("command"); + when(commandExtension.getCompleter()).thenReturn(completer); + when(commandExtension.getDescription()).thenReturn("description"); + commandWrapper = new CommandWrapper(commandExtension); + } + + @Test + public void fillsCommandDescriptionsLocalOnly() { + completerWrapper = commandWrapper.getCompleter(true); + var candidates = new ArrayList(); + when(commandLine.getArguments()).thenReturn(new String[] { "" }); + when(commandLine.getCursorArgumentIndex()).thenReturn(0); + when(commandLine.getArgumentPosition()).thenReturn(0); + + completerWrapper.completeCandidates(session, commandLine, candidates); + assertEquals(1, candidates.size()); + assertEquals("command", candidates.get(0).value()); + assertEquals("description", candidates.get(0).descr()); + } + + @Test + public void fillsCommandDescriptionsLocalAndGlobal() { + completerWrapper = commandWrapper.getCompleter(false); + var candidates = new ArrayList(); + when(commandLine.getArguments()).thenReturn(new String[] { "" }); + when(commandLine.getCursorArgumentIndex()).thenReturn(0); + when(commandLine.getArgumentPosition()).thenReturn(0); + + completerWrapper.completeCandidates(session, commandLine, candidates); + assertEquals(2, candidates.size()); + assertEquals("command", candidates.get(0).value()); + assertEquals("description", candidates.get(0).descr()); + + assertEquals("openhab:command", candidates.get(1).value()); + assertEquals("description", candidates.get(1).descr()); + } + + @Test + public void completeCandidatesCompletesArguments() { + completerWrapper = commandWrapper.getCompleter(true); + var candidates = new ArrayList(); + when(commandLine.getArguments()).thenReturn(new String[] { "command", "subcmd" }); + when(commandLine.getCursorArgumentIndex()).thenReturn(1); + when(commandLine.getArgumentPosition()).thenReturn(6); + when(commandLine.getBufferPosition()).thenReturn(14); + when(completer.complete(new String[] { "subcmd" }, 0, 6, new ArrayList<>())).thenReturn(false); + + completerWrapper.completeCandidates(session, commandLine, candidates); + assertTrue(candidates.isEmpty()); + } + + @Test + public void doesntCallCompleterForOtherCommands() { + completerWrapper = commandWrapper.getCompleter(true); + var candidates = new ArrayList(); + when(commandLine.getArguments()).thenReturn(new String[] { "somethingElse", "" }); + when(commandLine.getCursorArgumentIndex()).thenReturn(1); + verifyNoInteractions(completer); + + assertEquals(-1, completerWrapper.complete(session, commandLine, candidates)); + assertTrue(candidates.isEmpty()); + } + + @Test + public void callsCompleterWithProperlyScopedArguments() { + completerWrapper = commandWrapper.getCompleter(true); + var candidates = new ArrayList(); + when(commandLine.getArguments()).thenReturn(new String[] { "command", "subcmd" }); + when(commandLine.getCursorArgumentIndex()).thenReturn(1); + when(commandLine.getArgumentPosition()).thenReturn(6); + when(completer.complete(new String[] { "subcmd" }, 0, 6, new ArrayList<>())).thenReturn(false); + + assertEquals(-1, completerWrapper.complete(session, commandLine, candidates)); + assertTrue(candidates.isEmpty()); + } + + @Test + public void callsCompleterForGlobalForm() { + completerWrapper = commandWrapper.getCompleter(false); + var candidates = new ArrayList(); + when(commandLine.getArguments()).thenReturn(new String[] { "openhab:command", "subcmd" }); + when(commandLine.getCursorArgumentIndex()).thenReturn(1); + when(commandLine.getArgumentPosition()).thenReturn(6); + when(completer.complete(new String[] { "subcmd" }, 0, 6, new ArrayList<>())).thenReturn(false); + + assertEquals(-1, completerWrapper.complete(session, commandLine, candidates)); + assertTrue(candidates.isEmpty()); + } +} diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleCommandCompleter.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleCommandCompleter.java new file mode 100644 index 0000000000..b2b81e91ef --- /dev/null +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/ConsoleCommandCompleter.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.console; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Implementing this interface allows a {@link ConsoleCommandExtension} to + * provide completions for the user as they write commands. + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public interface ConsoleCommandCompleter { + /** + * Populate possible completion candidates. + * + * @param args An array of all arguments to be passed to the ConsoleCommandExtension's execute method + * @param cursorArgumentIndex the argument index the cursor is currently in + * @param cursorPosition the position of the cursor within the argument + * @param candidates a list to fill with possible completion candidates + * @return if a candidate was found + */ + boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates); +} diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/StringsCompleter.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/StringsCompleter.java new file mode 100644 index 0000000000..3a71f719f5 --- /dev/null +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/StringsCompleter.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.console; + +import java.util.Collection; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Completer for a set of strings. + * + * It will provide candidate completions for whichever argument the cursor is located in. + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class StringsCompleter implements ConsoleCommandCompleter { + private final SortedSet strings; + private final boolean caseSensitive; + + public StringsCompleter() { + this(List.of(), false); + } + + /** + * @param strings The set of valid strings to be completed + * @param caseSensitive if strings must match case sensitively when the user is typing them + */ + public StringsCompleter(final Collection strings, boolean caseSensitive) { + this.strings = new TreeSet<>(caseSensitive ? String::compareTo : String::compareToIgnoreCase); + this.caseSensitive = caseSensitive; + this.strings.addAll(strings); + } + + /** + * Gets the strings that are allowed for this completer, so that you can modify the set. + */ + public SortedSet getStrings() { + return strings; + } + + @Override + public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) { + String argument; + if (cursorArgumentIndex >= 0 && cursorArgumentIndex < args.length) { + argument = args[cursorArgumentIndex].substring(0, cursorPosition); + } else { + argument = ""; + } + + if (!caseSensitive) { + argument = argument.toLowerCase(); + } + + SortedSet matches = getStrings().tailSet(argument); + + for (String match : matches) { + String s = caseSensitive ? match : match.toLowerCase(); + if (!s.startsWith(argument)) { + break; + } + + candidates.add(match + " "); + } + + return !candidates.isEmpty(); + } +} diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/extensions/ConsoleCommandExtension.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/extensions/ConsoleCommandExtension.java index 04e2a35433..933f7fde3c 100644 --- a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/extensions/ConsoleCommandExtension.java +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/extensions/ConsoleCommandExtension.java @@ -15,7 +15,9 @@ package org.openhab.core.io.console.extensions; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.ConsoleCommandCompleter; /** * Client which provide a console command have to implement this interface @@ -53,4 +55,14 @@ public interface ConsoleCommandExtension { * @return the help texts for this extension */ List getUsages(); + + /** + * This method allows a {@link ConsoleCommandExtension} to provide an object to enable + * tab-completion functionality for the user. + * + * @return a {@link ConsoleCommandCompleter} object for this command + */ + default @Nullable ConsoleCommandCompleter getCompleter() { + return null; + } } diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandCompleter.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandCompleter.java new file mode 100644 index 0000000000..77a35e8396 --- /dev/null +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandCompleter.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.console.internal.extension; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.io.console.ConsoleCommandCompleter; +import org.openhab.core.io.console.StringsCompleter; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemNotUniqueException; +import org.openhab.core.items.ItemRegistry; + +/** + * Console command completer for send and update + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class ItemConsoleCommandCompleter implements ConsoleCommandCompleter { + private final ItemRegistry itemRegistry; + private final @Nullable Function[]> dataTypeGetter; + + public ItemConsoleCommandCompleter(ItemRegistry itemRegistry) { + this.itemRegistry = itemRegistry; + this.dataTypeGetter = null; + } + + public ItemConsoleCommandCompleter(ItemRegistry itemRegistry, Function[]> dataTypeGetter) { + this.itemRegistry = itemRegistry; + this.dataTypeGetter = dataTypeGetter; + } + + @Override + @SuppressWarnings("unchecked") + public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) { + if (cursorArgumentIndex <= 0) { + return new StringsCompleter( + itemRegistry.getAll().stream().map(i -> i.getName()).collect(Collectors.toList()), true) + .complete(args, cursorArgumentIndex, cursorPosition, candidates); + } + var localDataTypeGetter = dataTypeGetter; + if (cursorArgumentIndex == 1 && localDataTypeGetter != null) { + try { + Item item = itemRegistry.getItemByPattern(args[0]); + Stream> enums = Stream.of(localDataTypeGetter.apply(item)).filter(Class::isEnum); + Stream> enumConstants = enums.flatMap( + t -> Stream.of(Objects.requireNonNull(((Class>) t).getEnumConstants()))); + return new StringsCompleter(enumConstants.map(Object::toString).collect(Collectors.toList()), false) + .complete(args, cursorArgumentIndex, cursorPosition, candidates); + } catch (ItemNotFoundException | ItemNotUniqueException e) { + return false; + } + } + return false; + } +} diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtension.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtension.java index 318f49755b..6b0ae9517d 100644 --- a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtension.java +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtension.java @@ -17,9 +17,13 @@ import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.ConsoleCommandCompleter; +import org.openhab.core.io.console.StringsCompleter; import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; import org.openhab.core.io.console.extensions.ConsoleCommandExtension; import org.openhab.core.items.GenericItem; @@ -48,6 +52,42 @@ public class ItemConsoleCommandExtension extends AbstractConsoleCommandExtension private static final String SUBCMD_REMOVE = "remove"; private static final String SUBCMD_ADDTAG = "addTag"; private static final String SUBCMD_RMTAG = "rmTag"; + private static final StringsCompleter SUBCMD_COMPLETER = new StringsCompleter( + List.of(SUBCMD_LIST, SUBCMD_CLEAR, SUBCMD_REMOVE, SUBCMD_ADDTAG, SUBCMD_RMTAG), false); + + private class ItemConsoleCommandCompleter implements ConsoleCommandCompleter { + @Override + public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) { + if (cursorArgumentIndex <= 0) { + return SUBCMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); + } + if (cursorArgumentIndex == 1) { + Collection items; + switch (args[0]) { + case SUBCMD_ADDTAG: + case SUBCMD_RMTAG: + items = managedItemProvider.getAll(); + break; + case SUBCMD_REMOVE: + items = itemRegistry.getAll(); + break; + default: + return false; + } + return new StringsCompleter(items.stream().map(i -> i.getName()).collect(Collectors.toList()), true) + .complete(args, cursorArgumentIndex, cursorPosition, candidates); + } + if (cursorArgumentIndex == 2 && args[0].equals(SUBCMD_RMTAG)) { + Item item = managedItemProvider.get(args[1]); + if (item == null) { + return false; + } + return new StringsCompleter(item.getTags(), true).complete(args, cursorArgumentIndex, cursorPosition, + candidates); + } + return false; + } + } private final ItemRegistry itemRegistry; private final ManagedItemProvider managedItemProvider; @@ -129,6 +169,11 @@ public class ItemConsoleCommandExtension extends AbstractConsoleCommandExtension } } + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return new ItemConsoleCommandCompleter(); + } + private void handleTags(final Consumer func, final T tag, GenericItem gItem, Console console) { // allow adding/removing of tags only for managed items if (managedItemProvider.get(gItem.getName()) != null) { diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/SendConsoleCommandExtension.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/SendConsoleCommandExtension.java index 4a3e4202e3..6602bb64bc 100644 --- a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/SendConsoleCommandExtension.java +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/SendConsoleCommandExtension.java @@ -13,10 +13,13 @@ package org.openhab.core.io.console.internal.extension; import java.util.List; +import java.util.Objects; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.events.EventPublisher; import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.ConsoleCommandCompleter; import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; import org.openhab.core.io.console.extensions.ConsoleCommandExtension; import org.openhab.core.items.Item; @@ -78,7 +81,7 @@ public class SendConsoleCommandExtension extends AbstractConsoleCommandExtension console.print(" " + acceptedType.getSimpleName()); if (acceptedType.isEnum()) { console.print(": "); - for (Object e : acceptedType.getEnumConstants()) { + for (Object e : Objects.requireNonNull(acceptedType.getEnumConstants())) { console.print(e + " "); } } @@ -100,4 +103,10 @@ public class SendConsoleCommandExtension extends AbstractConsoleCommandExtension printUsage(console); } } + + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return new ItemConsoleCommandCompleter(itemRegistry, + (Item i) -> i.getAcceptedCommandTypes().toArray(Class[]::new)); + } } diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/StatusConsoleCommandExtension.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/StatusConsoleCommandExtension.java index 563c9e495e..6cdd46d173 100644 --- a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/StatusConsoleCommandExtension.java +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/StatusConsoleCommandExtension.java @@ -15,7 +15,9 @@ package org.openhab.core.io.console.internal.extension; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.ConsoleCommandCompleter; import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; import org.openhab.core.io.console.extensions.ConsoleCommandExtension; import org.openhab.core.items.Item; @@ -69,4 +71,9 @@ public class StatusConsoleCommandExtension extends AbstractConsoleCommandExtensi printUsage(console); } } + + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return new ItemConsoleCommandCompleter(itemRegistry); + } } diff --git a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/UpdateConsoleCommandExtension.java b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/UpdateConsoleCommandExtension.java index a76866f994..39701a1d61 100644 --- a/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/UpdateConsoleCommandExtension.java +++ b/bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/UpdateConsoleCommandExtension.java @@ -15,8 +15,10 @@ package org.openhab.core.io.console.internal.extension; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.events.EventPublisher; import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.ConsoleCommandCompleter; import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; import org.openhab.core.io.console.extensions.ConsoleCommandExtension; import org.openhab.core.items.Item; @@ -93,4 +95,10 @@ public class UpdateConsoleCommandExtension extends AbstractConsoleCommandExtensi printUsage(console); } } + + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return new ItemConsoleCommandCompleter(itemRegistry, + (Item i) -> i.getAcceptedDataTypes().toArray(Class[]::new)); + } } diff --git a/bundles/org.openhab.core.io.console/src/test/java/org/openhab/core/io/console/StringsCompleterTest.java b/bundles/org.openhab.core.io.console/src/test/java/org/openhab/core/io/console/StringsCompleterTest.java new file mode 100644 index 0000000000..685cc42d65 --- /dev/null +++ b/bundles/org.openhab.core.io.console/src/test/java/org/openhab/core/io/console/StringsCompleterTest.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.console; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class StringsCompleterTest { + @Test + public void completeSimple() { + var sc = new StringsCompleter(List.of("def", "abc", "ghi"), false); + var candidates = new ArrayList(); + + // positive match + assertTrue(sc.complete(new String[] { "a" }, 0, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("abc ", candidates.get(0)); + candidates.clear(); + + // negative match + assertFalse(sc.complete(new String[] { "z" }, 0, 1, candidates)); + assertTrue(candidates.isEmpty()); + + // case insensitive + assertTrue(sc.complete(new String[] { "A" }, 0, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("abc ", candidates.get(0)); + candidates.clear(); + + // second argument + assertTrue(sc.complete(new String[] { "a", "d" }, 1, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("def ", candidates.get(0)); + candidates.clear(); + + // cursor not at end of word (truncates rest) + assertTrue(sc.complete(new String[] { "a", "dg" }, 1, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("def ", candidates.get(0)); + candidates.clear(); + + // first argument when second is present + assertTrue(sc.complete(new String[] { "a", "d" }, 0, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("abc ", candidates.get(0)); + } + + @Test + public void caseSensitive() { + var sc = new StringsCompleter(List.of("dEf", "ABc", "ghi"), true); + var candidates = new ArrayList(); + + assertFalse(sc.complete(new String[] { "D" }, 0, 1, candidates)); + assertTrue(candidates.isEmpty()); + + assertFalse(sc.complete(new String[] { "ab" }, 0, 1, candidates)); + assertTrue(candidates.isEmpty()); + + assertTrue(sc.complete(new String[] { "AB" }, 0, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("ABc ", candidates.get(0)); + } + + @Test + public void multipleCandidates() { + var sc = new StringsCompleter(List.of("abcde", "bcde", "abcdef", "abcdd", "abcdee", "abcdf"), false); + var candidates = new ArrayList(); + + assertTrue(sc.complete(new String[] { "abcd" }, 0, 4, candidates)); + assertEquals(5, candidates.size()); + assertEquals("abcdd ", candidates.get(0)); + assertEquals("abcde ", candidates.get(1)); + assertEquals("abcdee ", candidates.get(2)); + assertEquals("abcdef ", candidates.get(3)); + assertEquals("abcdf ", candidates.get(4)); + candidates.clear(); + + assertTrue(sc.complete(new String[] { "abcde" }, 0, 5, candidates)); + assertEquals(3, candidates.size()); + assertEquals("abcde ", candidates.get(0)); + assertEquals("abcdee ", candidates.get(1)); + assertEquals("abcdef ", candidates.get(2)); + candidates.clear(); + + assertTrue(sc.complete(new String[] { "abcdee" }, 0, 6, candidates)); + assertEquals(1, candidates.size()); + assertEquals("abcdee ", candidates.get(0)); + } +} diff --git a/bundles/org.openhab.core.io.console/src/test/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandCompleterTest.java b/bundles/org.openhab.core.io.console/src/test/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandCompleterTest.java new file mode 100644 index 0000000000..c9a7a72ce2 --- /dev/null +++ b/bundles/org.openhab.core.io.console/src/test/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandCompleterTest.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.console.internal.extension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemNotUniqueException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.library.items.SwitchItem; + +/** + * @author Cody Cutrer - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class ItemConsoleCommandCompleterTest { + private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; + + @BeforeEach + public void setup() { + List items = List.of(new SwitchItem("Item1"), new SwitchItem("Item2"), new SwitchItem("JItem1")); + when(itemRegistryMock.getAll()).thenReturn(items); + } + + private void mockGetItemByPattern() throws ItemNotFoundException, ItemNotUniqueException { + when(itemRegistryMock.getItemByPattern(anyString())).thenAnswer(invocation -> { + switch ((String) invocation.getArguments()[0]) { + case "Item1": + return itemRegistryMock.getAll().iterator().next(); + default: + throw new ItemNotFoundException("It"); + } + }); + } + + @Test + public void completeItems() throws ItemNotFoundException, ItemNotUniqueException { + var completer = new ItemConsoleCommandCompleter(itemRegistryMock); + var candidates = new ArrayList(); + + assertTrue(completer.complete(new String[] { "It" }, 0, 2, candidates)); + assertEquals(2, candidates.size()); + assertEquals("Item1 ", candidates.get(0)); + assertEquals("Item2 ", candidates.get(1)); + candidates.clear(); + + assertTrue(completer.complete(new String[] { "JI" }, 0, 2, candidates)); + assertEquals(1, candidates.size()); + assertEquals("JItem1 ", candidates.get(0)); + candidates.clear(); + + // case sensitive + assertFalse(completer.complete(new String[] { "it" }, 0, 2, candidates)); + assertTrue(candidates.isEmpty()); + + // doesn't complete anything when we're not referring to the current argument + assertFalse(completer.complete(new String[] { "It", "It" }, 1, 2, candidates)); + assertTrue(candidates.isEmpty()); + + // doesn't complete anything for the second argument + assertFalse(completer.complete(new String[] { "Item1", "" }, 1, 0, candidates)); + assertTrue(candidates.isEmpty()); + } + + @Test + public void completeSend() throws ItemNotFoundException, ItemNotUniqueException { + var completer = new ItemConsoleCommandCompleter(itemRegistryMock, + i -> i.getAcceptedCommandTypes().toArray(Class[]::new)); + var candidates = new ArrayList(); + mockGetItemByPattern(); + + // Can't find the item; no commands at all + assertFalse(completer.complete(new String[] { "It", "O" }, 1, 1, candidates)); + assertTrue(candidates.isEmpty()); + + assertTrue(completer.complete(new String[] { "Item1", "" }, 1, 0, candidates)); + assertEquals(3, candidates.size()); + assertEquals("OFF ", candidates.get(0)); + assertEquals("ON ", candidates.get(1)); + assertEquals("REFRESH ", candidates.get(2)); + candidates.clear(); + + // case insensitive + assertTrue(completer.complete(new String[] { "Item1", "o" }, 1, 1, candidates)); + assertEquals(2, candidates.size()); + assertEquals("OFF ", candidates.get(0)); + assertEquals("ON ", candidates.get(1)); + } + + @Test + public void completeUpdate() throws ItemNotFoundException, ItemNotUniqueException { + var completer = new ItemConsoleCommandCompleter(itemRegistryMock, + i -> i.getAcceptedDataTypes().toArray(Class[]::new)); + var candidates = new ArrayList(); + mockGetItemByPattern(); + + // Can't find the item; no commands at all + assertFalse(completer.complete(new String[] { "It", "O" }, 1, 1, candidates)); + assertTrue(candidates.isEmpty()); + + assertTrue(completer.complete(new String[] { "Item1", "" }, 1, 0, candidates)); + assertEquals(4, candidates.size()); + assertEquals("NULL ", candidates.get(0)); + assertEquals("OFF ", candidates.get(1)); + assertEquals("ON ", candidates.get(2)); + assertEquals("UNDEF ", candidates.get(3)); + candidates.clear(); + + // case insensitive + assertTrue(completer.complete(new String[] { "Item1", "o" }, 1, 1, candidates)); + assertEquals(2, candidates.size()); + assertEquals("OFF ", candidates.get(0)); + assertEquals("ON ", candidates.get(1)); + } +} diff --git a/bundles/org.openhab.core.io.console/src/test/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtensionTest.java b/bundles/org.openhab.core.io.console/src/test/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtensionTest.java new file mode 100644 index 0000000000..104f6650d1 --- /dev/null +++ b/bundles/org.openhab.core.io.console/src/test/java/org/openhab/core/io/console/internal/extension/ItemConsoleCommandExtensionTest.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.console.internal.extension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.io.console.ConsoleCommandCompleter; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.ManagedItemProvider; +import org.openhab.core.library.items.SwitchItem; + +/** + * @author Cody Cutrer - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class ItemConsoleCommandExtensionTest { + private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; + private @Mock @NonNullByDefault({}) ManagedItemProvider managedItemProviderMock; + private @NonNullByDefault({}) ConsoleCommandCompleter completer; + + @BeforeEach + public void setup() { + completer = new ItemConsoleCommandExtension(itemRegistryMock, managedItemProviderMock).getCompleter(); + } + + @Test + public void completeSubcommands() { + var candidates = new ArrayList(); + + assertTrue(completer.complete(new String[] { "" }, 0, 0, candidates)); + assertEquals(5, candidates.size()); + assertEquals("addTag ", candidates.get(0)); + assertEquals("clear ", candidates.get(1)); + assertEquals("list ", candidates.get(2)); + assertEquals("remove ", candidates.get(3)); + assertEquals("rmTag ", candidates.get(4)); + candidates.clear(); + + assertTrue(completer.complete(new String[] { "A", "Item1" }, 0, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("addTag ", candidates.get(0)); + } + + @Test + public void completeManagedItems() { + List items = List.of(new SwitchItem("Item1")); + when(managedItemProviderMock.getAll()).thenReturn(items); + var candidates = new ArrayList(); + + assertFalse(completer.complete(new String[] { "bogus", "I" }, 1, 1, candidates)); + assertTrue(candidates.isEmpty()); + + assertTrue(completer.complete(new String[] { "addTag", "I" }, 0, 6, candidates)); + assertEquals(1, candidates.size()); + assertEquals("addTag ", candidates.get(0)); + candidates.clear(); + + assertTrue(completer.complete(new String[] { "addTag", "I" }, 1, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("Item1 ", candidates.get(0)); + candidates.clear(); + + assertTrue(completer.complete(new String[] { "rmTag", "I" }, 1, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("Item1 ", candidates.get(0)); + } + + @Test + public void completeAllItems() { + List items = List.of(new SwitchItem("Item2")); + when(itemRegistryMock.getAll()).thenReturn(items); + var candidates = new ArrayList(); + + assertTrue(completer.complete(new String[] { "remove", "I" }, 0, 6, candidates)); + assertEquals(1, candidates.size()); + assertEquals("remove ", candidates.get(0)); + candidates.clear(); + + assertTrue(completer.complete(new String[] { "remove", "I" }, 1, 1, candidates)); + assertEquals(1, candidates.size()); + assertEquals("Item2 ", candidates.get(0)); + } + + @Test + public void completeRmTag() { + var item3 = new SwitchItem("Item3"); + var item4 = new SwitchItem("Item4"); + item3.addTag("Tag1"); + when(managedItemProviderMock.get(anyString())).thenAnswer(invocation -> { + switch ((String) invocation.getArguments()[0]) { + case "Item3": + return item3; + case "Item4": + return item4; + default: + return null; + } + }); + var candidates = new ArrayList(); + + // wrong sub-command + assertFalse(completer.complete(new String[] { "addTag", "Item3", "" }, 2, 0, candidates)); + assertTrue(candidates.isEmpty()); + + // Item doesn't exist + assertFalse(completer.complete(new String[] { "rmTag", "Item2", "" }, 2, 0, candidates)); + assertTrue(candidates.isEmpty()); + + // Item has no tags + assertFalse(completer.complete(new String[] { "rmTag", "Item4", "" }, 2, 0, candidates)); + assertTrue(candidates.isEmpty()); + + assertTrue(completer.complete(new String[] { "rmTag", "Item3", "" }, 2, 0, candidates)); + assertEquals(1, candidates.size()); + assertEquals("Tag1 ", candidates.get(0)); + } +}