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/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
index d48c8e3408510cacc148e8071af95994610869a6..ae7ca7fbed3a1a4532a78bb8a029bb7fc642bcb3 100644
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
@@ -106,6 +106,13 @@ public class PaperConfig {
         fixEntityPositionDesync = getBoolean("settings.fix-entity-position-desync", fixEntityPositionDesync);
     }
 
+    public static boolean enableBrigadierConsoleHighlighting = true;
+    public static boolean enableBrigadierConsoleCompletions = true;
+    private static void consoleSettings() {
+        enableBrigadierConsoleHighlighting = getBoolean("settings.console.enable-brigadier-highlighting", enableBrigadierConsoleHighlighting);
+        enableBrigadierConsoleCompletions = getBoolean("settings.console.enable-brigadier-completions", enableBrigadierConsoleCompletions);
+    }
+
     public static void registerCommands() {
         for (Map.Entry<String, Command> entry : commands.entrySet()) {
             MinecraftServer.getServer().server.getCommandMap().register(entry.getKey(), "Paper", entry.getValue());
diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
index a4070b59e261f0f1ac4beec47b11492f4724bf27..e0b1f0671d16ddddcb6725acd25a1d1d69e42701 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 (com.destroystokyo.paper.PaperConfig.enableBrigadierConsoleHighlighting) {
+            builder.highlighter(new io.papermc.paper.console.BrigadierCommandHighlighter(this.server, this.server.createCommandSourceStack()));
+        }
+        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..d3f80b5dcd366c5b8a48cb885d825d243b01ac4c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/console/BrigadierCommandCompleter.java
@@ -0,0 +1,95 @@
+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.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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion.completion;
+
+public final class BrigadierCommandCompleter {
+    private final CommandSourceStack commandSourceStack;
+    private final DedicatedServer server;
+
+    public BrigadierCommandCompleter(final @NonNull DedicatedServer server, final @NonNull CommandSourceStack commandSourceStack) {
+        this.server = server;
+        this.commandSourceStack = commandSourceStack;
+    }
+
+    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(), existing);
+            return;
+        }
+        final CommandDispatcher<CommandSourceStack> dispatcher = this.server.getCommands().getDispatcher();
+        final ParseResults<CommandSourceStack> results = dispatcher.parse(prepareStringReader(line.line()), this.commandSourceStack);
+        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.PLAIN.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..5ab8365b806dd035800ba9b449c9bc9233772d13
--- /dev/null
+++ b/src/main/java/io/papermc/paper/console/BrigadierCommandHighlighter.java
@@ -0,0 +1,64 @@
+package io.papermc.paper.console;
+
+import com.mojang.brigadier.ParseResults;
+import com.mojang.brigadier.context.ParsedCommandNode;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+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 CommandSourceStack commandSourceStack;
+    private final DedicatedServer server;
+
+    public BrigadierCommandHighlighter(final @NonNull DedicatedServer server, final @NonNull CommandSourceStack commandSourceStack) {
+        this.server = server;
+        this.commandSourceStack = commandSourceStack;
+    }
+
+    @Override
+    public AttributedString highlight(final @NonNull LineReader reader, final @NonNull String buffer) {
+        final AttributedStringBuilder builder = new AttributedStringBuilder();
+        final ParseResults<CommandSourceStack> results = this.server.getCommands().getDispatcher().parse(BrigadierCommandCompleter.prepareStringReader(buffer), this.commandSourceStack);
+        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 02d7b16f81ebf9f902a36d4f31802b20d1820d6e..77f49329cc903ba687c60032ba4b21786f79a56b 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -182,7 +182,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
 
         thread.setDaemon(true);
         thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(DedicatedServer.LOGGER));
-        thread.start();
+        // thread.start(); // Paper - 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\"");
@@ -216,6 +216,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
             DedicatedServer.LOGGER.error("Unable to load server configuration", e);
             return false;
         }
+        thread.start(); // Paper - start console thread after MinecraftServer.console & PaperConfig are initialized
         com.destroystokyo.paper.PaperConfig.registerCommands();
         com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // load version history now
         io.papermc.paper.util.ObfHelper.INSTANCE.getClass(); // load mappings for stacktrace deobf and etc.
diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
index 14cd8ae69d9b25dc5edad4ff96ff4a9acb1f22cb..b3484487fa8baa4d1dd6c595586fb26a01a2153d 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
 
     public ConsoleCommandCompleter(DedicatedServer server) { // Paper - CraftServer -> DedicatedServer
         this.server = server;
+        this.brigadierCompleter = new io.papermc.paper.console.BrigadierCommandCompleter(this.server, this.server.createCommandSourceStack()); // Paper
     }
 
     // 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
                 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
 }
