aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJake Potrebic <[email protected]>2024-09-06 13:37:09 -0700
committerGitHub <[email protected]>2024-09-06 13:37:09 -0700
commit805a97444afd3f9d95f3fc4e5674915722c89066 (patch)
treeca9ca93e1c379e07d4b588a67128f9904acac21d
parent10f5879992060f15cf42f877c441afa6f6663ce5 (diff)
downloadPaper-805a97444afd3f9d95f3fc4e5674915722c89066.tar.gz
Paper-805a97444afd3f9d95f3fc4e5674915722c89066.zip
Improve console completion with brig suggestions (#9251)
* Improve console completion with brig suggestions * silence warning * small fixes * squashed
-rw-r--r--patches/server/0508-Enhance-console-tab-completions-for-brigadier-comman.patch200
1 files 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 <[email protected]>
Date: Tue, 30 Mar 2021 16:06:08 -0700
Subject: [PATCH] Enhance console tab completions for brigadier commands
+Co-authored-by: Jake Potrebic <[email protected]>
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<CommandSourceStack> 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<Candidate> candidates,
+ final @NonNull List<Suggestion> brigSuggestions,
-+ final @NonNull List<Completion> existing
++ final @NonNull List<Completion> existing,
++ final @NonNull ParseContext context
+ ) {
-+ final List<Completion> 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<LineReader.Option, Boolean> 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<String, List<Candidate>> candidates = new HashMap<>();
++ for (final Map.Entry<String, List<Candidate>> 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<CommandSourceStack> results = this.server.getCommands().getDispatcher().parse(prepareStringReader(line), this.server.createCommandSourceStack());
++ final ImmutableStringReader reader = results.getReader();
++ final List<String> words = new ArrayList<>();
++ CommandContextBuilder<CommandSourceStack> currentContext = results.getContext();
++ int currentWordIdx = -1;
++ int wordIdx = -1;
++ int inWordCursor = -1;
++ if (currentContext.getRange().getLength() > 0) {
++ do {
++ for (final ParsedCommandNode<CommandSourceStack> 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<String> 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