From 805a97444afd3f9d95f3fc4e5674915722c89066 Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Fri, 6 Sep 2024 13:37:09 -0700 Subject: [PATCH] Improve console completion with brig suggestions (#9251) * Improve console completion with brig suggestions * silence warning * small fixes * squashed --- ...tab-completions-for-brigadier-comman.patch | 200 ++++++++++++++++-- 1 file changed, 181 insertions(+), 19 deletions(-) diff --git a/patches/server/0508-Enhance-console-tab-completions-for-brigadier-comman.patch b/patches/server/0508-Enhance-console-tab-completions-for-brigadier-comman.patch index 701ba9042a..0ec775f4f6 100644 --- a/patches/server/0508-Enhance-console-tab-completions-for-brigadier-comman.patch +++ b/patches/server/0508-Enhance-console-tab-completions-for-brigadier-comman.patch @@ -3,12 +3,22 @@ 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 +Co-authored-by: Jake Potrebic 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 +index a4070b59e261f0f1ac4beec47b11492f4724bf27..6ee39b534b8d992655bc0cef3c299d12cbae0034 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 { +@@ -1,5 +1,8 @@ + package com.destroystokyo.paper.console; + ++import io.papermc.paper.configuration.GlobalConfiguration; ++import io.papermc.paper.console.BrigadierCompletionMatcher; ++import io.papermc.paper.console.BrigadierConsoleParser; + import net.minecraft.server.dedicated.DedicatedServer; + import net.minecrell.terminalconsole.SimpleTerminalConsole; + import org.bukkit.craftbukkit.command.ConsoleCommandCompleter; +@@ -16,11 +19,20 @@ public final class PaperConsole extends SimpleTerminalConsole { @Override protected LineReader buildReader(LineReaderBuilder builder) { @@ -22,18 +32,24 @@ index a4070b59e261f0f1ac4beec47b11492f4724bf27..c5d5648f4ca603ef2b1df723b58f9caf + if (io.papermc.paper.configuration.GlobalConfiguration.get().console.enableBrigadierHighlighting) { + builder.highlighter(new io.papermc.paper.console.BrigadierCommandHighlighter(this.server)); + } ++ if (GlobalConfiguration.get().console.enableBrigadierCompletions) { ++ System.setProperty("org.jline.reader.support.parsedline", "true"); // to hide a warning message about the parser not supporting ++ builder.parser(new BrigadierConsoleParser(this.server)); ++ builder.completionMatcher(new BrigadierCompletionMatcher()); ++ } + 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 +index 0000000000000000000000000000000000000000..2fe00debd08c0f5fdb254edff62a79ced6fb09c2 --- /dev/null +++ b/src/main/java/io/papermc/paper/console/BrigadierCommandCompleter.java -@@ -0,0 +1,99 @@ +@@ -0,0 +1,127 @@ +package io.papermc.paper.console; + ++import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent; +import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion; +import com.google.common.base.Suppliers; +import com.mojang.brigadier.CommandDispatcher; @@ -45,10 +61,12 @@ index 0000000000000000000000000000000000000000..7a4f4c0a0fdcabd2bc4aa26dc9d76fc1 +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; ++import net.kyori.adventure.text.Component; +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.checkerframework.checker.nullness.qual.Nullable; +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; @@ -69,7 +87,7 @@ index 0000000000000000000000000000000000000000..7a4f4c0a0fdcabd2bc4aa26dc9d76fc1 + 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); ++ this.addCandidates(candidates, Collections.emptyList(), existing, new ParseContext(line.line(), 0)); + return; + } + final CommandDispatcher dispatcher = this.server.getCommands().getDispatcher(); @@ -77,41 +95,57 @@ index 0000000000000000000000000000000000000000..7a4f4c0a0fdcabd2bc4aa26dc9d76fc1 + this.addCandidates( + candidates, + dispatcher.getCompletionSuggestions(results, line.cursor()).join().getList(), -+ existing ++ existing, ++ new ParseContext(line.line(), results.getContext().findSuggestionContext(line.cursor()).startPos) + ); + } + + private void addCandidates( + final @NonNull List candidates, + final @NonNull List brigSuggestions, -+ final @NonNull List existing ++ final @NonNull List existing, ++ final @NonNull ParseContext context + ) { -+ final List completions = new ArrayList<>(); -+ brigSuggestions.forEach(it -> completions.add(toCompletion(it))); -+ for (final Completion completion : existing) { ++ brigSuggestions.forEach(it -> { ++ if (it.getText().isEmpty()) return; ++ candidates.add(toCandidate(it, context)); ++ }); ++ for (final AsyncTabCompleteEvent.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 Candidate toCandidate(final Suggestion suggestion, final @NonNull ParseContext context) { ++ Component tooltip = null; ++ if (suggestion.getTooltip() != null) { ++ tooltip = PaperAdventure.asAdventure(ComponentUtils.fromMessage(suggestion.getTooltip())); ++ } ++ return toCandidate(context.line.substring(context.suggestionStart, suggestion.getRange().getStart()) + suggestion.getText(), tooltip); ++ } ++ + 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( ++ return toCandidate(completion.suggestion(), completion.tooltip()); ++ } ++ ++ private static @NonNull Candidate toCandidate(final @NonNull String suggestionText, final @Nullable Component tooltip) { ++ final String suggestionTooltip = PaperAdventure.ANSI_SERIALIZER.serializeOr(tooltip, null); ++ //noinspection SpellCheckingInspection ++ return new PaperCandidate( + suggestionText, + suggestionText, + null, + suggestionTooltip, + null, + null, ++ /* ++ in an ideal world, this would sometimes be true if the suggestion represented the final possible value for a word. ++ Like for `/execute alig`, pressing enter on align would add a trailing space if this value was true. But not all ++ suggestions should add spaces after, like `/execute as @`, accepting any suggestion here would be valid, but its also ++ valid to have a `[` following the selector ++ */ + false + ); + } @@ -130,6 +164,15 @@ index 0000000000000000000000000000000000000000..7a4f4c0a0fdcabd2bc4aa26dc9d76fc1 + } + return stringReader; + } ++ ++ private record ParseContext(String line, int suggestionStart) { ++ } ++ ++ public static final class PaperCandidate extends Candidate { ++ public PaperCandidate(final String value, final String display, final String group, final String descr, final String suffix, final String key, final boolean complete) { ++ super(value, display, group, descr, suffix, key, complete); ++ } ++ } +} 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 @@ -207,6 +250,125 @@ index 0000000000000000000000000000000000000000..dd9d77d7c7f1a5a130a1f4c15e5b1e68 + @Override + public void setErrorIndex(final int errorIndex) {} +} +diff --git a/src/main/java/io/papermc/paper/console/BrigadierCompletionMatcher.java b/src/main/java/io/papermc/paper/console/BrigadierCompletionMatcher.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1e8028a43db0ff1d5b22d06ef12c1c32d992c09c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/console/BrigadierCompletionMatcher.java +@@ -0,0 +1,27 @@ ++package io.papermc.paper.console; ++ ++import com.google.common.collect.Iterables; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import org.jline.reader.Candidate; ++import org.jline.reader.CompletingParsedLine; ++import org.jline.reader.LineReader; ++import org.jline.reader.impl.CompletionMatcherImpl; ++ ++public class BrigadierCompletionMatcher extends CompletionMatcherImpl { ++ ++ @Override ++ protected void defaultMatchers(final Map options, final boolean prefix, final CompletingParsedLine line, final boolean caseInsensitive, final int errors, final String originalGroupName) { ++ super.defaultMatchers(options, prefix, line, caseInsensitive, errors, originalGroupName); ++ this.matchers.addFirst(m -> { ++ final Map> candidates = new HashMap<>(); ++ for (final Map.Entry> entry : m.entrySet()) { ++ if (Iterables.all(entry.getValue(), BrigadierCommandCompleter.PaperCandidate.class::isInstance)) { ++ candidates.put(entry.getKey(), entry.getValue()); ++ } ++ } ++ return candidates; ++ }); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/console/BrigadierConsoleParser.java b/src/main/java/io/papermc/paper/console/BrigadierConsoleParser.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6e211580b1bc6e2c5ec6f2641b0cf91862985db1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/console/BrigadierConsoleParser.java +@@ -0,0 +1,80 @@ ++package io.papermc.paper.console; ++ ++import com.mojang.brigadier.ImmutableStringReader; ++import com.mojang.brigadier.ParseResults; ++import com.mojang.brigadier.context.CommandContextBuilder; ++import com.mojang.brigadier.context.ParsedCommandNode; ++import com.mojang.brigadier.context.StringRange; ++import java.util.ArrayList; ++import java.util.List; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.server.dedicated.DedicatedServer; ++import org.jline.reader.ParsedLine; ++import org.jline.reader.Parser; ++import org.jline.reader.SyntaxError; ++ ++import static io.papermc.paper.console.BrigadierCommandCompleter.prepareStringReader; ++ ++public class BrigadierConsoleParser implements Parser { ++ ++ private final DedicatedServer server; ++ ++ public BrigadierConsoleParser(DedicatedServer server) { ++ this.server = server; ++ } ++ ++ @Override ++ public ParsedLine parse(final String line, final int cursor, final ParseContext context) throws SyntaxError { ++ final ParseResults results = this.server.getCommands().getDispatcher().parse(prepareStringReader(line), this.server.createCommandSourceStack()); ++ final ImmutableStringReader reader = results.getReader(); ++ final List words = new ArrayList<>(); ++ CommandContextBuilder currentContext = results.getContext(); ++ int currentWordIdx = -1; ++ int wordIdx = -1; ++ int inWordCursor = -1; ++ if (currentContext.getRange().getLength() > 0) { ++ do { ++ for (final ParsedCommandNode node : currentContext.getNodes()) { ++ final StringRange nodeRange = node.getRange(); ++ String current = nodeRange.get(reader); ++ words.add(current); ++ currentWordIdx++; ++ if (wordIdx == -1 && nodeRange.getStart() <= cursor && nodeRange.getEnd() >= cursor) { ++ // if cursor is in the middle of a parsed word/node ++ wordIdx = currentWordIdx; ++ inWordCursor = cursor - nodeRange.getStart(); ++ } ++ } ++ currentContext = currentContext.getChild(); ++ } while (currentContext != null); ++ } ++ final String leftovers = reader.getRemaining(); ++ if (!leftovers.isEmpty() && leftovers.isBlank()) { ++ // if brig didn't consume the whole line, and everything else is blank, add a new empty word ++ currentWordIdx++; ++ words.add(""); ++ if (wordIdx == -1) { ++ wordIdx = currentWordIdx; ++ inWordCursor = 0; ++ } ++ } else if (!leftovers.isEmpty()) { ++ // if there are unparsed leftovers, add a new word with the remaining input ++ currentWordIdx++; ++ words.add(leftovers); ++ if (wordIdx == -1) { ++ wordIdx = currentWordIdx; ++ inWordCursor = cursor - reader.getCursor(); ++ } ++ } ++ if (wordIdx == -1) { ++ currentWordIdx++; ++ words.add(""); ++ wordIdx = currentWordIdx; ++ inWordCursor = 0; ++ } ++ return new BrigadierParsedLine(words.get(wordIdx), inWordCursor, wordIdx, words, line, cursor); ++ } ++ ++ record BrigadierParsedLine(String word, int wordCursor, int wordIndex, List words, String line, int cursor) implements ParsedLine { ++ } ++} diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index d5153f804cfcfd1a70c46975e3fb1e50c8a82999..764395fe8e49d811294ca82887fee91ca6cd01fc 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java