From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Tue, 30 Mar 2021 16:06:08 -0700
Subject: [PATCH] Enhance console tab completions for brigadier commands


diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
index a4070b59e261f0f1ac4beec47b11492f4724bf27..c5d5648f4ca603ef2b1df723b58f9caf4dd3c722 100644
--- a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
+++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
@@ -16,11 +16,15 @@ public final class PaperConsole extends SimpleTerminalConsole {
 
     @Override
     protected LineReader buildReader(LineReaderBuilder builder) {
-        return super.buildReader(builder
+        builder
                 .appName("Paper")
                 .variable(LineReader.HISTORY_FILE, java.nio.file.Paths.get(".console_history"))
                 .completer(new ConsoleCommandCompleter(this.server))
-        );
+                .option(LineReader.Option.COMPLETE_IN_WORD, true);
+        if (io.papermc.paper.configuration.GlobalConfiguration.get().console.enableBrigadierHighlighting) {
+            builder.highlighter(new io.papermc.paper.console.BrigadierCommandHighlighter(this.server));
+        }
+        return super.buildReader(builder);
     }
 
     @Override
diff --git a/src/main/java/io/papermc/paper/console/BrigadierCommandCompleter.java b/src/main/java/io/papermc/paper/console/BrigadierCommandCompleter.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a4f4c0a0fdcabd2bc4aa26dc9d76fc150b8435c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/console/BrigadierCommandCompleter.java
@@ -0,0 +1,99 @@
+package io.papermc.paper.console;
+
+import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion;
+import com.google.common.base.Suppliers;
+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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.network.chat.ComponentUtils;
+import net.minecraft.server.dedicated.DedicatedServer;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.jline.reader.Candidate;
+import org.jline.reader.LineReader;
+import org.jline.reader.ParsedLine;
+
+import static com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion.completion;
+
+public final class BrigadierCommandCompleter {
+    private final Supplier<CommandSourceStack> commandSourceStack;
+    private final DedicatedServer server;
+
+    public BrigadierCommandCompleter(final @NonNull DedicatedServer server) {
+        this.server = server;
+        this.commandSourceStack = Suppliers.memoize(this.server::createCommandSourceStack);
+    }
+
+    public void complete(final @NonNull LineReader reader, final @NonNull ParsedLine line, final @NonNull List<Candidate> candidates, final @NonNull List<Completion> existing) {
+        //noinspection ConstantConditions
+        if (this.server.overworld() == null) { // check if overworld is null, as worlds haven't been loaded yet
+            return;
+        } else if (!io.papermc.paper.configuration.GlobalConfiguration.get().console.enableBrigadierCompletions) {
+            this.addCandidates(candidates, Collections.emptyList(), existing);
+            return;
+        }
+        final CommandDispatcher<CommandSourceStack> dispatcher = this.server.getCommands().getDispatcher();
+        final ParseResults<CommandSourceStack> results = dispatcher.parse(prepareStringReader(line.line()), this.commandSourceStack.get());
+        this.addCandidates(
+            candidates,
+            dispatcher.getCompletionSuggestions(results, line.cursor()).join().getList(),
+            existing
+        );
+    }
+
+    private void addCandidates(
+        final @NonNull List<Candidate> candidates,
+        final @NonNull List<Suggestion> brigSuggestions,
+        final @NonNull List<Completion> existing
+    ) {
+        final List<Completion> completions = new ArrayList<>();
+        brigSuggestions.forEach(it -> completions.add(toCompletion(it)));
+        for (final Completion completion : existing) {
+            if (completion.suggestion().isEmpty() || brigSuggestions.stream().anyMatch(it -> it.getText().equals(completion.suggestion()))) {
+                continue;
+            }
+            completions.add(completion);
+        }
+        for (final Completion completion : completions) {
+            if (completion.suggestion().isEmpty()) {
+                continue;
+            }
+            candidates.add(toCandidate(completion));
+        }
+    }
+
+    private static @NonNull Candidate toCandidate(final @NonNull Completion completion) {
+        final String suggestionText = completion.suggestion();
+        final String suggestionTooltip = PaperAdventure.ANSI_SERIALIZER.serializeOr(completion.tooltip(), null);
+        return new Candidate(
+            suggestionText,
+            suggestionText,
+            null,
+            suggestionTooltip,
+            null,
+            null,
+            false
+        );
+    }
+
+    private static @NonNull Completion toCompletion(final @NonNull Suggestion suggestion) {
+        if (suggestion.getTooltip() == null) {
+            return completion(suggestion.getText());
+        }
+        return completion(suggestion.getText(), PaperAdventure.asAdventure(ComponentUtils.fromMessage(suggestion.getTooltip())));
+    }
+
+    static @NonNull StringReader prepareStringReader(final @NonNull String line) {
+        final StringReader stringReader = new StringReader(line);
+        if (stringReader.canRead() && stringReader.peek() == '/') {
+            stringReader.skip();
+        }
+        return stringReader;
+    }
+}
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
index 0000000000000000000000000000000000000000..dd9d77d7c7f1a5a130a1f4c15e5b1e68ae3753e1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/console/BrigadierCommandHighlighter.java
@@ -0,0 +1,70 @@
+package io.papermc.paper.console;
+
+import com.google.common.base.Suppliers;
+import com.mojang.brigadier.ParseResults;
+import com.mojang.brigadier.context.ParsedCommandNode;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.server.dedicated.DedicatedServer;
+import org.checkerframework.checker.nullness.qual.NonNull;
+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;
+
+public final class BrigadierCommandHighlighter implements Highlighter {
+    private static final int[] COLORS = {AttributedStyle.CYAN, AttributedStyle.YELLOW, AttributedStyle.GREEN, AttributedStyle.MAGENTA, /* Client uses GOLD here, not BLUE, however there is no GOLD AttributedStyle. */ AttributedStyle.BLUE};
+    private final Supplier<CommandSourceStack> commandSourceStack;
+    private final DedicatedServer server;
+
+    public BrigadierCommandHighlighter(final @NonNull DedicatedServer server) {
+        this.server = server;
+        this.commandSourceStack = Suppliers.memoize(this.server::createCommandSourceStack);
+    }
+
+    @Override
+    public AttributedString highlight(final @NonNull LineReader reader, final @NonNull String buffer) {
+        //noinspection ConstantConditions
+        if (this.server.overworld() == null) { // check if overworld is null, as worlds haven't been loaded yet
+            return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
+        }
+        final AttributedStringBuilder builder = new AttributedStringBuilder();
+        final ParseResults<CommandSourceStack> results = this.server.getCommands().getDispatcher().parse(BrigadierCommandCompleter.prepareStringReader(buffer), this.commandSourceStack.get());
+        int pos = 0;
+        if (buffer.startsWith("/")) {
+            builder.append("/", AttributedStyle.DEFAULT);
+            pos = 1;
+        }
+        int component = -1;
+        for (final ParsedCommandNode<CommandSourceStack> node : results.getContext().getLastChild().getNodes()) {
+            if (node.getRange().getStart() >= buffer.length()) {
+                break;
+            }
+            final int start = node.getRange().getStart();
+            final int end = Math.min(node.getRange().getEnd(), buffer.length());
+            builder.append(buffer.substring(pos, start), AttributedStyle.DEFAULT);
+            if (node.getNode() instanceof LiteralCommandNode) {
+                builder.append(buffer.substring(start, end), AttributedStyle.DEFAULT);
+            } else {
+                if (++component >= COLORS.length) {
+                    component = 0;
+                }
+                builder.append(buffer.substring(start, end), AttributedStyle.DEFAULT.foreground(COLORS[component]));
+            }
+            pos = end;
+        }
+        if (pos < buffer.length()) {
+            builder.append((buffer.substring(pos)), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
+        }
+        return builder.toAttributedString();
+    }
+
+    @Override
+    public void setErrorPattern(final Pattern errorPattern) {}
+
+    @Override
+    public void setErrorIndex(final int errorIndex) {}
+}
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 4b00c14332f3d514f402fc82345a02d51c3ac58a..e3eaa004ecc4ddb57be4eeb993c37a7876cd55a1 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -186,7 +186,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
 
         thread.setDaemon(true);
         thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(DedicatedServer.LOGGER));
-        thread.start();
+        // thread.start(); // Paper - Enhance console tab completions for brigadier commands; moved down
         DedicatedServer.LOGGER.info("Starting minecraft server version {}", SharedConstants.getCurrentVersion().getName());
         if (Runtime.getRuntime().maxMemory() / 1024L / 1024L < 512L) {
             DedicatedServer.LOGGER.warn("To start the server with more ram, launch it as \"java -Xmx1024M -Xms1024M -jar minecraft_server.jar\"");
@@ -219,6 +219,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
         this.getPlayerList().loadAndSaveFiles(); // Must be after convertNames
         // Paper end - fix converting txt to json file
         org.spigotmc.WatchdogThread.doStart(org.spigotmc.SpigotConfig.timeoutTime, org.spigotmc.SpigotConfig.restartOnCrash); // Paper - start watchdog thread
+        thread.start(); // Paper - Enhance console tab completions for brigadier commands; start console thread after MinecraftServer.console & PaperConfig are initialized
         io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command
         com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics
         com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now
diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
index 8f82041f0482df22a6a9ea38d50d56228131775d..3e93a6c489972ff2b4ecff3d83cc72b2d5c970f8 100644
--- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
+++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
@@ -18,9 +18,11 @@ import org.bukkit.event.server.TabCompleteEvent;
 
 public class ConsoleCommandCompleter implements Completer {
     private final DedicatedServer server; // Paper - CraftServer -> DedicatedServer
+    private final io.papermc.paper.console.BrigadierCommandCompleter brigadierCompleter; // Paper - Enhance console tab completions for brigadier commands
 
     public ConsoleCommandCompleter(DedicatedServer server) { // Paper - CraftServer -> DedicatedServer
         this.server = server;
+        this.brigadierCompleter = new io.papermc.paper.console.BrigadierCommandCompleter(this.server); // Paper - Enhance console tab completions for brigadier commands
     }
 
     // Paper start - Change method signature for JLine update
@@ -64,7 +66,7 @@ public class ConsoleCommandCompleter implements Completer {
                 }
             }
 
-            if (!completions.isEmpty()) {
+            if (false && !completions.isEmpty()) {
                 for (final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion completion : completions) {
                     if (completion.suggestion().isEmpty()) {
                         continue;
@@ -80,6 +82,7 @@ public class ConsoleCommandCompleter implements Completer {
                     ));
                 }
             }
+            this.addCompletions(reader, line, candidates, completions);
             return;
         }
 
@@ -99,10 +102,12 @@ public class ConsoleCommandCompleter implements Completer {
         try {
             List<String> offers = waitable.get();
             if (offers == null) {
+                this.addCompletions(reader, line, candidates, Collections.emptyList()); // Paper - Enhance console tab completions for brigadier commands
                 return; // Paper - Method returns void
             }
 
             // Paper start - JLine update
+            /*
             for (String completion : offers) {
                 if (completion.isEmpty()) {
                     continue;
@@ -110,6 +115,8 @@ public class ConsoleCommandCompleter implements Completer {
 
                 candidates.add(new Candidate(completion));
             }
+             */
+            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
@@ -138,5 +145,9 @@ public class ConsoleCommandCompleter implements Completer {
         }
         return false;
     }
+
+    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
 }