From 7bc482249f3c49c1ef52f39ac73c48e83e125ad5 Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Fri, 7 May 2021 16:12:03 -0700
Subject: [PATCH] Allow for Component suggestion tooltips in
 AsyncTabCompleteEvent (#5504)

---
 ...ent-suggestion-tooltips-in-AsyncTabC.patch | 407 ++++++++++++++++++
 ...ent-suggestion-tooltips-in-AsyncTabC.patch | 132 ++++++
 ...tab-completions-for-brigadier-comman.patch |  67 +--
 3 files changed, 562 insertions(+), 44 deletions(-)
 create mode 100644 Spigot-API-Patches/Allow-for-Component-suggestion-tooltips-in-AsyncTabC.patch
 create mode 100644 Spigot-Server-Patches/Allow-for-Component-suggestion-tooltips-in-AsyncTabC.patch

diff --git a/Spigot-API-Patches/Allow-for-Component-suggestion-tooltips-in-AsyncTabC.patch b/Spigot-API-Patches/Allow-for-Component-suggestion-tooltips-in-AsyncTabC.patch
new file mode 100644
index 0000000000..8385584cf5
--- /dev/null
+++ b/Spigot-API-Patches/Allow-for-Component-suggestion-tooltips-in-AsyncTabC.patch
@@ -0,0 +1,407 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
+Date: Thu, 1 Apr 2021 00:34:41 -0700
+Subject: [PATCH] Allow for Component suggestion tooltips in
+ AsyncTabCompleteEvent
+
+
+diff --git a/src/main/java/com/destroystokyo/paper/event/server/AsyncTabCompleteEvent.java b/src/main/java/com/destroystokyo/paper/event/server/AsyncTabCompleteEvent.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/com/destroystokyo/paper/event/server/AsyncTabCompleteEvent.java
++++ b/src/main/java/com/destroystokyo/paper/event/server/AsyncTabCompleteEvent.java
+@@ -0,0 +0,0 @@
+ package com.destroystokyo.paper.event.server;
+ 
+ import com.google.common.collect.ImmutableList;
++import io.papermc.paper.util.TransformingRandomAccessList;
++import net.kyori.adventure.text.Component;
++import net.kyori.examination.Examinable;
++import net.kyori.examination.ExaminableProperty;
++import net.kyori.examination.string.StringExaminer;
+ import org.apache.commons.lang.Validate;
+ import org.bukkit.Location;
+ import org.bukkit.command.Command;
+@@ -0,0 +0,0 @@ import org.bukkit.event.HandlerList;
+ 
+ import java.util.ArrayList;
+ import java.util.List;
++import java.util.stream.Stream;
+ import org.jetbrains.annotations.NotNull;
+ import org.jetbrains.annotations.Nullable;
+ 
+@@ -0,0 +0,0 @@ public class AsyncTabCompleteEvent extends Event implements Cancellable {
+     private final boolean isCommand;
+     @Nullable
+     private final Location loc;
+-    @NotNull private List<String> completions;
++    private final List<Completion> completions = new ArrayList<>();
++    private final List<String> stringCompletions = new TransformingRandomAccessList<>(
++        this.completions,
++        Completion::suggestion,
++        Completion::completion
++    );
+     private boolean cancelled;
+     private boolean handled = false;
+     private boolean fireSyncHandler = true;
+ 
++    public AsyncTabCompleteEvent(@NotNull CommandSender sender, @NotNull String buffer, boolean isCommand, @Nullable Location loc) {
++        super(true);
++        this.sender = sender;
++        this.buffer = buffer;
++        this.isCommand = isCommand;
++        this.loc = loc;
++    }
++
++    @Deprecated
+     public AsyncTabCompleteEvent(@NotNull CommandSender sender, @NotNull List<String> completions, @NotNull String buffer, boolean isCommand, @Nullable Location loc) {
+         super(true);
+         this.sender = sender;
+-        this.completions = completions;
++        this.completions.addAll(fromStrings(completions));
+         this.buffer = buffer;
+         this.isCommand = isCommand;
+         this.loc = loc;
+@@ -0,0 +0,0 @@ public class AsyncTabCompleteEvent extends Event implements Cancellable {
+      */
+     @NotNull
+     public List<String> getCompletions() {
+-        return completions;
++        return this.stringCompletions;
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ public class AsyncTabCompleteEvent extends Event implements Cancellable {
+      * @param completions the new completions
+      */
+     public void setCompletions(@NotNull List<String> completions) {
++        if (completions == this.stringCompletions) {
++            return;
++        }
+         Validate.notNull(completions);
+-        this.completions = new ArrayList<>(completions);
++        this.completions.clear();
++        this.completions.addAll(fromStrings(completions));
++    }
++
++    /**
++     * The list of {@link Completion completions} which will be offered to the sender, in order.
++     * This list is mutable and reflects what will be offered.
++     * <p>
++     * If this collection is not empty after the event is fired, then
++     * the standard process of calling {@link Command#tabComplete(CommandSender, String, String[])}
++     * or current player names will not be called.
++     *
++     * @return a list of offered completions
++     */
++    public @NotNull List<@NotNull Completion> completions() {
++        return this.completions;
++    }
++
++    /**
++     * Set the {@link Completion completions} offered, overriding any already set.
++     * If this collection is not empty after the event is fired, then
++     * the standard process of calling {@link Command#tabComplete(CommandSender, String, String[])}
++     * or current player names will not be called.
++     * <p>
++     * The passed collection will be cloned to a new List. You must call {{@link #completions()}} to mutate from here
++     *
++     * @param newCompletions the new completions
++     */
++    public void completions(final @NotNull List<@NotNull Completion> newCompletions) {
++        Validate.notNull(newCompletions, "new completions");
++        this.completions.clear();
++        this.completions.addAll(newCompletions);
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ public class AsyncTabCompleteEvent extends Event implements Cancellable {
+     public static HandlerList getHandlerList() {
+         return handlers;
+     }
++
++    private static @NotNull List<Completion> fromStrings(final @NotNull List<String> strings) {
++        final List<Completion> list = new ArrayList<>();
++        for (final String it : strings) {
++            list.add(new CompletionImpl(it, null));
++        }
++        return list;
++    }
++
++    /**
++     * A rich tab completion, consisting of a string suggestion, and a nullable {@link Component} tooltip.
++     */
++    public interface Completion extends Examinable {
++        /**
++         * Get the suggestion string for this {@link Completion}.
++         *
++         * @return suggestion string
++         */
++        @NotNull String suggestion();
++
++        /**
++         * Get the suggestion tooltip for this {@link Completion}.
++         *
++         * @return tooltip component
++         */
++        @Nullable Component tooltip();
++
++        @Override
++        default @NotNull Stream<? extends ExaminableProperty> examinableProperties() {
++            return Stream.of(ExaminableProperty.of("suggestion", this.suggestion()), ExaminableProperty.of("tooltip", this.tooltip()));
++        }
++
++        /**
++         * Create a new {@link Completion} from a suggestion string.
++         *
++         * @param suggestion suggestion string
++         * @return new completion instance
++         */
++        static @NotNull Completion completion(final @NotNull String suggestion) {
++            return new CompletionImpl(suggestion, null);
++        }
++
++        /**
++         * Create a new {@link Completion} from a suggestion string and a tooltip {@link Component}.
++         *
++         * <p>If the provided component is null, the suggestion will not have a tooltip.</p>
++         *
++         * @param suggestion suggestion string
++         * @param tooltip    tooltip component, or null
++         * @return new completion instance
++         */
++        static @NotNull Completion completion(final @NotNull String suggestion, final @Nullable Component tooltip) {
++            return new CompletionImpl(suggestion, tooltip);
++        }
++    }
++
++    static final class CompletionImpl implements Completion {
++        private final String suggestion;
++        private final Component tooltip;
++
++        CompletionImpl(final @NotNull String suggestion, final @Nullable Component tooltip) {
++            this.suggestion = suggestion;
++            this.tooltip = tooltip;
++        }
++
++        @Override
++        public @NotNull String suggestion() {
++            return this.suggestion;
++        }
++
++        @Override
++        public @Nullable Component tooltip() {
++            return this.tooltip;
++        }
++
++        @Override
++        public boolean equals(final @Nullable Object o) {
++            if (this == o) {
++                return true;
++            }
++            if (o == null || this.getClass() != o.getClass()) {
++                return false;
++            }
++            final CompletionImpl that = (CompletionImpl) o;
++            return this.suggestion.equals(that.suggestion)
++                && java.util.Objects.equals(this.tooltip, that.tooltip);
++        }
++
++        @Override
++        public int hashCode() {
++            return java.util.Objects.hash(this.suggestion, this.tooltip);
++        }
++
++        @Override
++        public @NotNull String toString() {
++            return StringExaminer.simpleEscaping().examine(this);
++        }
++    }
+ }
+diff --git a/src/main/java/io/papermc/paper/util/TransformingRandomAccessList.java b/src/main/java/io/papermc/paper/util/TransformingRandomAccessList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/TransformingRandomAccessList.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.util;
++
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.AbstractList;
++import java.util.Iterator;
++import java.util.List;
++import java.util.ListIterator;
++import java.util.RandomAccess;
++import java.util.function.Function;
++import java.util.function.Predicate;
++
++import static com.google.common.base.Preconditions.checkNotNull;
++
++/**
++ * Modified version of the Guava class with the same name to support add operations.
++ *
++ * @param <F> backing list element type
++ * @param <T> transformed list element type
++ */
++public final class TransformingRandomAccessList<F, T> extends AbstractList<T> implements RandomAccess {
++    final List<F> fromList;
++    final Function<? super F, ? extends T> toFunction;
++    final Function<? super T, ? extends F> fromFunction;
++
++    /**
++     * Create a new {@link TransformingRandomAccessList}.
++     *
++     * @param fromList     backing list
++     * @param toFunction   function mapping backing list element type to transformed list element type
++     * @param fromFunction function mapping transformed list element type to backing list element type
++     */
++    public TransformingRandomAccessList(
++        final @NonNull List<F> fromList,
++        final @NonNull Function<? super F, ? extends T> toFunction,
++        final @NonNull Function<? super T, ? extends F> fromFunction
++    ) {
++        this.fromList = checkNotNull(fromList);
++        this.toFunction = checkNotNull(toFunction);
++        this.fromFunction = checkNotNull(fromFunction);
++    }
++
++    @Override
++    public void clear() {
++        this.fromList.clear();
++    }
++
++    @Override
++    public T get(int index) {
++        return this.toFunction.apply(this.fromList.get(index));
++    }
++
++    @Override
++    public @NotNull Iterator<T> iterator() {
++        return this.listIterator();
++    }
++
++    @Override
++    public @NotNull ListIterator<T> listIterator(int index) {
++        return new TransformedListIterator<F, T>(this.fromList.listIterator(index)) {
++            @Override
++            T transform(F from) {
++                return TransformingRandomAccessList.this.toFunction.apply(from);
++            }
++
++            @Override
++            F transformBack(T from) {
++                return TransformingRandomAccessList.this.fromFunction.apply(from);
++            }
++        };
++    }
++
++    @Override
++    public boolean isEmpty() {
++        return this.fromList.isEmpty();
++    }
++
++    @Override
++    public boolean removeIf(Predicate<? super T> filter) {
++        checkNotNull(filter);
++        return this.fromList.removeIf(element -> filter.test(this.toFunction.apply(element)));
++    }
++
++    @Override
++    public T remove(int index) {
++        return this.toFunction.apply(this.fromList.remove(index));
++    }
++
++    @Override
++    public int size() {
++        return this.fromList.size();
++    }
++
++    @Override
++    public T set(int i, T t) {
++        return this.toFunction.apply(this.fromList.set(i, this.fromFunction.apply(t)));
++    }
++
++    @Override
++    public void add(int i, T t) {
++        this.fromList.add(i, this.fromFunction.apply(t));
++    }
++
++    static abstract class TransformedListIterator<F, T> implements ListIterator<T>, Iterator<T> {
++        final Iterator<F> backingIterator;
++
++        TransformedListIterator(ListIterator<F> backingIterator) {
++            this.backingIterator = checkNotNull((Iterator<F>) backingIterator);
++        }
++
++        private ListIterator<F> backingIterator() {
++            return cast(this.backingIterator);
++        }
++
++        static <A> ListIterator<A> cast(Iterator<A> iterator) {
++            return (ListIterator<A>) iterator;
++        }
++
++        @Override
++        public final boolean hasPrevious() {
++            return this.backingIterator().hasPrevious();
++        }
++
++        @Override
++        public final T previous() {
++            return this.transform(this.backingIterator().previous());
++        }
++
++        @Override
++        public final int nextIndex() {
++            return this.backingIterator().nextIndex();
++        }
++
++        @Override
++        public final int previousIndex() {
++            return this.backingIterator().previousIndex();
++        }
++
++        @Override
++        public void set(T element) {
++            this.backingIterator().set(this.transformBack(element));
++        }
++
++        @Override
++        public void add(T element) {
++            this.backingIterator().add(this.transformBack(element));
++        }
++
++        abstract T transform(F from);
++
++        abstract F transformBack(T to);
++
++        @Override
++        public final boolean hasNext() {
++            return this.backingIterator.hasNext();
++        }
++
++        @Override
++        public final T next() {
++            return this.transform(this.backingIterator.next());
++        }
++
++        @Override
++        public final void remove() {
++            this.backingIterator.remove();
++        }
++    }
++}
+diff --git a/src/test/java/org/bukkit/AnnotationTest.java b/src/test/java/org/bukkit/AnnotationTest.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/test/java/org/bukkit/AnnotationTest.java
++++ b/src/test/java/org/bukkit/AnnotationTest.java
+@@ -0,0 +0,0 @@ public class AnnotationTest {
+         // Generic functional interface
+         "org/bukkit/util/Consumer",
+         // Paper start
++        "io/papermc/paper/util/TransformingRandomAccessList",
++        "io/papermc/paper/util/TransformingRandomAccessList$TransformedListIterator",
+         // Timings history is broken in terms of nullability due to guavas Function defining that the param is NonNull
+         "co/aikar/timings/TimingHistory$2",
+         "co/aikar/timings/TimingHistory$2$1",
diff --git a/Spigot-Server-Patches/Allow-for-Component-suggestion-tooltips-in-AsyncTabC.patch b/Spigot-Server-Patches/Allow-for-Component-suggestion-tooltips-in-AsyncTabC.patch
new file mode 100644
index 0000000000..6ce190411e
--- /dev/null
+++ b/Spigot-Server-Patches/Allow-for-Component-suggestion-tooltips-in-AsyncTabC.patch
@@ -0,0 +1,132 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
+Date: Thu, 1 Apr 2021 00:34:02 -0700
+Subject: [PATCH] Allow for Component suggestion tooltips in
+ AsyncTabCompleteEvent
+
+
+diff --git a/src/main/java/net/minecraft/server/network/PlayerConnection.java b/src/main/java/net/minecraft/server/network/PlayerConnection.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/network/PlayerConnection.java
++++ b/src/main/java/net/minecraft/server/network/PlayerConnection.java
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+ 
+         // Paper start - async tab completion
+         com.destroystokyo.paper.event.server.AsyncTabCompleteEvent event;
+-        java.util.List<String> completions = new java.util.ArrayList<>();
+         String buffer = packetplayintabcomplete.c();
+-        event = new com.destroystokyo.paper.event.server.AsyncTabCompleteEvent(this.getPlayer(), completions,
++        event = new com.destroystokyo.paper.event.server.AsyncTabCompleteEvent(this.getPlayer(),
+                 buffer, true, null);
+         event.callEvent();
+-        completions = event.isCancelled() ? com.google.common.collect.ImmutableList.of() : event.getCompletions();
++        java.util.List<com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion> completions = event.isCancelled() ? com.google.common.collect.ImmutableList.of() : event.completions();
+         // If the event isn't handled, we can assume that we have no completions, and so we'll ask the server
+         if (!event.isHandled()) {
+             if (!event.isCancelled()) {
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                 });
+             }
+         } else if (!completions.isEmpty()) {
+-            com.mojang.brigadier.suggestion.SuggestionsBuilder builder = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packetplayintabcomplete.c(), stringreader.getTotalLength());
++            com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packetplayintabcomplete.c(), stringreader.getTotalLength());
+ 
+-            builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + 1);
+-            completions.forEach(builder::suggest);
++            final com.mojang.brigadier.suggestion.SuggestionsBuilder builder = builder0.createOffset(builder0.getInput().lastIndexOf(' ') + 1);
++            completions.forEach(completion -> {
++                if (completion.tooltip() == null) {
++                    builder.suggest(completion.suggestion());
++                } else {
++                    builder.suggest(completion.suggestion(), PaperAdventure.asVanilla(completion.tooltip()));
++                }
++            });
+             com.mojang.brigadier.suggestion.Suggestions suggestions = builder.buildFuture().join();
+             com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent suggestEvent = new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent(this.getPlayer(), suggestions, buffer);
+             suggestEvent.setCancelled(suggestions.isEmpty());
+diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
++++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
+@@ -0,0 +0,0 @@ public class ConsoleCommandCompleter implements Completer {
+         final CraftServer server = this.server.server;
+         final String buffer = line.line();
+         // Async Tab Complete
+-        com.destroystokyo.paper.event.server.AsyncTabCompleteEvent event;
+-        java.util.List<String> completions = new java.util.ArrayList<>();
+-        event = new com.destroystokyo.paper.event.server.AsyncTabCompleteEvent(server.getConsoleSender(), completions,
+-            buffer, true, null);
++        final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent event =
++            new com.destroystokyo.paper.event.server.AsyncTabCompleteEvent(server.getConsoleSender(), buffer, true, null);
+         event.callEvent();
+-        completions = event.isCancelled() ? com.google.common.collect.ImmutableList.of() : event.getCompletions();
++        final List<com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion> completions = event.isCancelled() ? com.google.common.collect.ImmutableList.of() : event.completions();
+ 
+         if (event.isCancelled() || event.isHandled()) {
+             // Still fire sync event with the provided completions, if someone is listening
+             if (!event.isCancelled() && TabCompleteEvent.getHandlerList().getRegisteredListeners().length > 0) {
+-                List<String> finalCompletions = completions;
++                List<com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion> finalCompletions = new java.util.ArrayList<>(completions);
+                 Waitable<List<String>> syncCompletions = new Waitable<List<String>>() {
+                     @Override
+                     protected List<String> evaluate() {
+-                        org.bukkit.event.server.TabCompleteEvent syncEvent = new org.bukkit.event.server.TabCompleteEvent(server.getConsoleSender(), buffer, finalCompletions);
++                        org.bukkit.event.server.TabCompleteEvent syncEvent = new org.bukkit.event.server.TabCompleteEvent(server.getConsoleSender(), buffer,
++                            finalCompletions.stream()
++                                .map(com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion::suggestion)
++                                .collect(java.util.stream.Collectors.toList()));
+                         return syncEvent.callEvent() ? syncEvent.getCompletions() : com.google.common.collect.ImmutableList.of();
+                     }
+                 };
+                 server.getServer().processQueue.add(syncCompletions);
+                 try {
+-                    completions = syncCompletions.get();
++                    final List<String> legacyCompletions = syncCompletions.get();
++                    completions.removeIf(it -> !legacyCompletions.contains(it.suggestion())); // remove any suggestions that were removed
++                    // add any new suggestions
++                    for (final String completion : legacyCompletions) {
++                        if (notNewSuggestion(completions, completion)) {
++                            continue;
++                        }
++                        completions.add(com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion.completion(completion));
++                    }
+                 } catch (InterruptedException | ExecutionException e1) {
+                     e1.printStackTrace();
+                 }
+             }
+ 
+             if (!completions.isEmpty()) {
+-                candidates.addAll(completions.stream().map(Candidate::new).collect(java.util.stream.Collectors.toList()));
++                for (final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion completion : completions) {
++                    if (completion.suggestion().isEmpty()) {
++                        continue;
++                    }
++                    candidates.add(new Candidate(
++                        completion.suggestion(),
++                        completion.suggestion(),
++                        null,
++                        io.papermc.paper.adventure.PaperAdventure.PLAIN.serializeOr(completion.tooltip(), null),
++                        null,
++                        null,
++                        false
++                    ));
++                }
+             }
+             return;
+         }
+@@ -0,0 +0,0 @@ public class ConsoleCommandCompleter implements Completer {
+             Thread.currentThread().interrupt();
+         }
+     }
++
++    // Paper start
++    private boolean notNewSuggestion(final List<com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion> completions, final String completion) {
++        for (final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion it : completions) {
++            if (it.suggestion().equals(completion)) {
++                return true;
++            }
++        }
++        return false;
++    }
++    // Paper end
+ }
diff --git a/Spigot-Server-Patches/Enhance-console-tab-completions-for-brigadier-comman.patch b/Spigot-Server-Patches/Enhance-console-tab-completions-for-brigadier-comman.patch
index 9681922d79..80071ff2d5 100644
--- a/Spigot-Server-Patches/Enhance-console-tab-completions-for-brigadier-comman.patch
+++ b/Spigot-Server-Patches/Enhance-console-tab-completions-for-brigadier-comman.patch
@@ -58,17 +58,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package io.papermc.paper.console;
 +
++import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion;
 +import com.mojang.brigadier.CommandDispatcher;
 +import com.mojang.brigadier.ParseResults;
 +import com.mojang.brigadier.StringReader;
 +import com.mojang.brigadier.suggestion.Suggestion;
 +import io.papermc.paper.adventure.PaperAdventure;
-+import net.kyori.adventure.text.Component;
 +import net.minecraft.commands.CommandListenerWrapper;
 +import net.minecraft.network.chat.ChatComponentUtils;
 +import net.minecraft.server.dedicated.DedicatedServer;
 +import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.checker.nullness.qual.Nullable;
 +import org.jline.reader.Candidate;
 +import org.jline.reader.LineReader;
 +import org.jline.reader.ParsedLine;
@@ -77,6 +76,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import java.util.Collections;
 +import java.util.List;
 +
++import static com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion.completion;
++
 +public final class BrigadierCommandCompleter {
 +    private final CommandListenerWrapper commandSourceStack;
 +    private final DedicatedServer server;
@@ -86,9 +87,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.commandSourceStack = commandSourceStack;
 +    }
 +
-+    public void complete(final @NonNull LineReader reader, final @NonNull ParsedLine line, final @NonNull List<Candidate> candidates, final @NonNull List<String> stringCompletions) {
++    public void complete(final @NonNull LineReader reader, final @NonNull ParsedLine line, final @NonNull List<Candidate> candidates, final @NonNull List<Completion> existing) {
 +        if (!com.destroystokyo.paper.PaperConfig.enableBrigadierConsoleCompletions) {
-+            this.addCandidates(candidates, Collections.emptyList(), stringCompletions);
++            this.addCandidates(candidates, Collections.emptyList(), existing);
 +            return;
 +        }
 +        final CommandDispatcher<CommandListenerWrapper> dispatcher = this.server.getCommandDispatcher().dispatcher();
@@ -96,25 +97,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.addCandidates(
 +            candidates,
 +            dispatcher.getCompletionSuggestions(results, line.cursor()).join().getList(),
-+            stringCompletions
++            existing
 +        );
 +    }
 +
 +    private void addCandidates(
 +        final @NonNull List<Candidate> candidates,
 +        final @NonNull List<Suggestion> brigSuggestions,
-+        final @NonNull List<String> stringSuggestions
++        final @NonNull List<Completion> existing
 +    ) {
 +        final List<Completion> completions = new ArrayList<>();
 +        brigSuggestions.forEach(it -> completions.add(toCompletion(it)));
-+        for (final String string : stringSuggestions) {
-+            if (string.isEmpty() || brigSuggestions.stream().anyMatch(it -> it.getText().equals(string))) {
++        for (final Completion completion : existing) {
++            if (completion.suggestion().isEmpty() || brigSuggestions.stream().anyMatch(it -> it.getText().equals(completion.suggestion()))) {
 +                continue;
 +            }
-+            completions.add(completion(string));
++            completions.add(completion);
 +        }
 +        for (final Completion completion : completions) {
-+            if (completion.completion().isEmpty()) {
++            if (completion.suggestion().isEmpty()) {
 +                continue;
 +            }
 +            candidates.add(toCandidate(completion));
@@ -122,7 +123,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    private static @NonNull Candidate toCandidate(final @NonNull Completion completion) {
-+        final String suggestionText = completion.completion();
++        final String suggestionText = completion.suggestion();
 +        final String suggestionTooltip = PaperAdventure.PLAIN.serializeOr(completion.tooltip(), null);
 +        return new Candidate(
 +            suggestionText,
@@ -149,32 +150,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +        return stringReader;
 +    }
-+
-+    static final class Completion {
-+        private final String completion;
-+        private final Component tooltip;
-+
-+        Completion(final @NonNull String completion, final @Nullable Component tooltip) {
-+            this.completion = completion;
-+            this.tooltip = tooltip;
-+        }
-+
-+        @NonNull String completion() {
-+            return this.completion;
-+        }
-+
-+        @Nullable Component tooltip() {
-+            return this.tooltip;
-+        }
-+    }
-+
-+    static @NonNull Completion completion(final @NonNull String completion) {
-+        return new Completion(completion, null);
-+    }
-+
-+    static @NonNull Completion completion(final @NonNull String completion, final @Nullable Component tooltip) {
-+        return new Completion(completion, tooltip);
-+    }
 +}
 diff --git a/src/main/java/io/papermc/paper/console/BrigadierCommandHighlighter.java b/src/main/java/io/papermc/paper/console/BrigadierCommandHighlighter.java
 new file mode 100644
@@ -296,7 +271,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
 -            if (!completions.isEmpty()) {
 +            if (false && !completions.isEmpty()) {
-                 candidates.addAll(completions.stream().map(Candidate::new).collect(java.util.stream.Collectors.toList()));
+                 for (final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion completion : completions) {
+                     if (completion.suggestion().isEmpty()) {
+                         continue;
+@@ -0,0 +0,0 @@ public class ConsoleCommandCompleter implements Completer {
+                     ));
+                 }
              }
 +            this.addCompletions(reader, line, candidates, completions);
              return;
@@ -320,18 +300,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  candidates.add(new Candidate(completion));
              }
 +             */
-+            this.addCompletions(reader, line, candidates, offers);
++            this.addCompletions(reader, line, candidates, offers.stream().map(com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion::completion).collect(java.util.stream.Collectors.toList()));
              // Paper end
  
              // Paper start - JLine handles cursor now
 @@ -0,0 +0,0 @@ public class ConsoleCommandCompleter implements Completer {
-             Thread.currentThread().interrupt();
          }
+         return false;
      }
 +
-+    // Paper start
-+    private void addCompletions(final LineReader reader, final ParsedLine line, final List<Candidate> candidates, final List<String> stringCompletions) {
-+        this.brigadierCompleter.complete(reader, line, candidates, stringCompletions);
++    private void addCompletions(final LineReader reader, final ParsedLine line, final List<Candidate> candidates, final List<com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion> existing) {
++        this.brigadierCompleter.complete(reader, line, candidates, existing);
 +    }
-+    // Paper end
+     // Paper end
  }