@SuppressWarnings("MixedMutabilityReturnType")
    private @NonNull CompletableFuture<SuggestionContext<C, ?>> getSuggestions(
            final @NonNull SuggestionContext<C, ?> context,
            final @NonNull CommandInput commandInput,
            final @NonNull CommandNode<C> root,
            final @NonNull Executor executor
    ) {
        // If the sender isn't allowed to access the root node, no suggestions are needed
        if (!this.determineAccess(context.commandContext().sender(), root).map(PermissionResult::allowed).orElse(false)) {
            return CompletableFuture.completedFuture(context);
        }

        final List<CommandNode<C>> children = root.children();
        final List<CommandNode<C>> staticArguments = children.stream()
                .filter(n -> n.component() != null)
                .filter(n -> n.component().type() == CommandComponent.ComponentType.LITERAL)
                .collect(Collectors.toList());

        if (!commandInput.isEmpty()) {
            commandInput.skipWhitespace(1);
        }

        // Try to see if any of the static literals can be parsed (matches exactly)
        // If so, enter that node of the command tree for deeper suggestions
        if (!staticArguments.isEmpty() && !commandInput.isEmpty(true /* ignoringWhitespace */)) {
            final CommandInput commandInputCopy = commandInput.copy();
            for (CommandNode<C> child : staticArguments) {
                final CommandComponent<C> childComponent = child.component();
                if (childComponent == null) {
                    continue;
                }

                final ArgumentParseResult<?> result = childComponent.parser().parse(
                        context.commandContext(),
                        commandInput
                );

                if (result.failure().isPresent()) {
                    commandInput.cursor(commandInputCopy.cursor());
                }

                if (!result.parsedValue().isPresent()) {
                    continue;
                }

                if (commandInput.isEmpty()) {
                    // We've already matched one exactly, no use looking further
                    break;
                }

                return this.getSuggestions(context, commandInput, child, executor);
            }

            // Restore original queue
            commandInput.cursor(commandInputCopy.cursor());
        }

        // Calculate suggestions for the literal arguments
        CompletableFuture<SuggestionContext<C, ?>> suggestionFuture = CompletableFuture.completedFuture(context);
        if (commandInput.remainingTokens() <= 1) {
            for (final CommandNode<C> node : staticArguments) {
                suggestionFuture = suggestionFuture
                        .thenCompose(ctx -> this.addSuggestionsForLiteralArgument(context, node, commandInput));
            }
        }

        // Calculate suggestions for the variable argument, if one exists
        for (final CommandNode<C> child : root.children()) {
            if (child.component() == null || child.component().type() == CommandComponent.ComponentType.LITERAL) {
                continue;
            }
            suggestionFuture = suggestionFuture
                    .thenCompose(ctx -> this.addSuggestionsForDynamicArgument(context, commandInput, child, executor, false));
        }

        return suggestionFuture;
    }
Returns suggestions from the input queue

@param <S>          suggestion type
@param context      Context instance
@param commandInput Input
@param mapper       suggestion mapper
@param executor     executor to schedule suggestion logic on
@return String suggestions. These should be filtered based on {@link String#startsWith(String)}
cloud-core/src/main/java/org/incendo/cloud/CommandTree.java