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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
@@ -0,0 +0,0 @@ public class PaperConfig {
     private static void fixEntityPositionDesync() {
         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);
+    }
 }
diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
+++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
@@ -0,0 +0,0 @@
 package com.destroystokyo.paper.console;
 
+import com.destroystokyo.paper.PaperConfig;
+import io.papermc.paper.console.BrigadierCommandHighlighter;
 import net.minecraft.server.dedicated.DedicatedServer;
 import net.minecrell.terminalconsole.SimpleTerminalConsole;
 import org.bukkit.craftbukkit.command.ConsoleCommandCompleter;
@@ -0,0 +0,0 @@ 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 (PaperConfig.enableBrigadierConsoleHighlighting) {
+            builder.highlighter(new BrigadierCommandHighlighter(this.server, this.server.getServerCommandListener()));
+        }
+        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..0000000000000000000000000000000000000000
--- /dev/null
+++ b/src/main/java/io/papermc/paper/console/BrigadierCommandCompleter.java
@@ -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.minecraft.commands.CommandListenerWrapper;
+import net.minecraft.network.chat.ChatComponentUtils;
+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 CommandListenerWrapper commandSourceStack;
+    private final DedicatedServer server;
+
+    public BrigadierCommandCompleter(final @NonNull DedicatedServer server, final @NonNull CommandListenerWrapper 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<CommandListenerWrapper> dispatcher = this.server.getCommandDispatcher().dispatcher();
+        final ParseResults<CommandListenerWrapper> 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(ChatComponentUtils.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..0000000000000000000000000000000000000000
--- /dev/null
+++ b/src/main/java/io/papermc/paper/console/BrigadierCommandHighlighter.java
@@ -0,0 +0,0 @@
+package io.papermc.paper.console;
+
+import com.mojang.brigadier.ParseResults;
+import com.mojang.brigadier.context.ParsedCommandNode;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import net.minecraft.commands.CommandListenerWrapper;
+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 CommandListenerWrapper commandSourceStack;
+    private final DedicatedServer server;
+
+    public BrigadierCommandHighlighter(final @NonNull DedicatedServer server, final @NonNull CommandListenerWrapper 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<CommandListenerWrapper> results = this.server.getCommandDispatcher().dispatcher().parse(BrigadierCommandCompleter.prepareStringReader(buffer), this.commandSourceStack);
+        int pos = 0;
+        if (buffer.startsWith("/")) {
+            builder.append("/", AttributedStyle.DEFAULT);
+            pos = 1;
+        }
+        int component = -1;
+        for (final ParsedCommandNode<CommandListenerWrapper> 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();
+    }
+}
diff --git a/src/main/java/net/minecraft/commands/CommandDispatcher.java b/src/main/java/net/minecraft/commands/CommandDispatcher.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/commands/CommandDispatcher.java
+++ b/src/main/java/net/minecraft/commands/CommandDispatcher.java
@@ -0,0 +0,0 @@ public class CommandDispatcher {
         };
     }
 
-    public com.mojang.brigadier.CommandDispatcher<CommandListenerWrapper> a() {
+    public com.mojang.brigadier.CommandDispatcher<CommandListenerWrapper> a() { return this.dispatcher(); } public com.mojang.brigadier.CommandDispatcher<CommandListenerWrapper> dispatcher() { // Paper - OBFHELPER
         return this.b;
     }
 
diff --git a/src/main/java/net/minecraft/network/chat/ChatComponentUtils.java b/src/main/java/net/minecraft/network/chat/ChatComponentUtils.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/chat/ChatComponentUtils.java
+++ b/src/main/java/net/minecraft/network/chat/ChatComponentUtils.java
@@ -0,0 +0,0 @@ public class ChatComponentUtils {
             ChatComponentText chatcomponenttext = new ChatComponentText("");
             boolean flag = true;
 
-            for (Iterator iterator = collection.iterator(); iterator.hasNext(); flag = false) {
+            for (Iterator<T> iterator = collection.iterator(); iterator.hasNext(); flag = false) { // Paper - decompile fix
                 T t0 = iterator.next();
 
                 if (!flag) {
@@ -0,0 +0,0 @@ public class ChatComponentUtils {
         return new ChatMessage("chat.square_brackets", new Object[]{ichatbasecomponent});
     }
 
-    public static IChatBaseComponent a(Message message) {
+    public static IChatBaseComponent a(Message message) { return fromMessage(message); } public static IChatBaseComponent fromMessage(final @org.checkerframework.checker.nullness.qual.NonNull Message message) { // Paper - OBFHELPER
         return (IChatBaseComponent) (message instanceof IChatBaseComponent ? (IChatBaseComponent) message : new ChatComponentText(message.getString()));
     }
 }
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 @@ 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.getServerCommandListener()); // Paper
     }
 
     // Paper start - Change method signature for JLine update
@@ -0,0 +0,0 @@ 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;
@@ -0,0 +0,0 @@ public class ConsoleCommandCompleter implements Completer {
                     ));
                 }
             }
+            this.addCompletions(reader, line, candidates, completions);
             return;
         }
 
@@ -0,0 +0,0 @@ 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;
@@ -0,0 +0,0 @@ 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
@@ -0,0 +0,0 @@ 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
 }