Skip to content

Commit

Permalink
GH-474 Suggestion tooltip (#461)
Browse files Browse the repository at this point in the history
* Support tooltip in paper

* ignore relocation, fix TAB_COMPLETION_CLASS

* fix unable to create a Completion correctly

* If the string is empty, there is no tooltip

* Support Fabric, Minestom and Sponge

* .

* Add tests

* Remove Completion

* Update Suggestion.java

* Update tests

* Add tooltipCollector

* fix test

* Update litecommands-unit/src/dev/rollczi/litecommands/unit/AssertSuggest.java

Co-authored-by: Norbert Dejlich <[email protected]>

* catch style

* remove tooltip method, fix assertSuggest, remove tooltipCollector

* tooltip for partial platform arguments

* Remove tooltips from the suggestion result.

* Make tooltip notnull

* Add missing collector for tooltips. Code style fixes.

* Use collector

* Make assertSuggest more simpler

* Use tooltip collector.

* Copy tooltip while appending

* Add preconditions for Suggestion.

* Make Suggestion#from internal.

* Support async suggestions for 1.12-1.15 paper

---------

Co-authored-by: Norbert Dejlich <[email protected]>
  • Loading branch information
huanmeng-qwq and Rollczi authored Nov 2, 2024
1 parent 9e7f1d8 commit f621def
Show file tree
Hide file tree
Showing 23 changed files with 383 additions and 71 deletions.
3 changes: 2 additions & 1 deletion examples/bukkit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
id("java")
id("com.github.johnrengelman.shadow") version "8.1.1"
id("net.minecrell.plugin-yml.bukkit") version "0.6.0"
id("xyz.jpenilla.run-paper") version "2.3.0"
id("xyz.jpenilla.run-paper") version "2.3.1"
}

version = "3.8.0"
Expand Down Expand Up @@ -55,4 +55,5 @@ sourceSets.test {

tasks.runServer {
minecraftVersion("1.21")
allJvmArgs = listOf("-DPaper.IgnoreJavaVersion=true")
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
import dev.rollczi.litecommands.platform.PlatformSender;
import dev.rollczi.litecommands.platform.PlatformSuggestionListener;
import dev.rollczi.litecommands.argument.suggester.input.SuggestionInput;
import dev.rollczi.litecommands.suggestion.Suggestion;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class BukkitCommand extends Command {

Expand Down Expand Up @@ -54,10 +57,12 @@ public boolean execute(@NotNull CommandSender sender, @NotNull String alias, Str
@Override
public @NotNull List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, String[] args) {
if (this.syncTabComplete) {
CompletableFuture<List<String>> future = this.suggest(sender, alias, args);
CompletableFuture<Set<Suggestion>> future = this.suggest(sender, alias, args);

try {
return future.get(0, TimeUnit.MILLISECONDS);
return future.get(0, TimeUnit.MILLISECONDS).stream()
.map(suggestion -> suggestion.multilevel())
.collect(Collectors.toList());
}
catch (TimeoutException exception) {
if (settings.syncSuggestionWarning()) {
Expand All @@ -75,12 +80,12 @@ public boolean execute(@NotNull CommandSender sender, @NotNull String alias, Str
return Collections.emptyList();
}

public CompletableFuture<List<String>> suggest(CommandSender sender, String alias, String[] args) {
public CompletableFuture<Set<Suggestion>> suggest(CommandSender sender, String alias, String[] args) {
SuggestionInput<?> input = SuggestionInput.raw(args);
PlatformSender platformSender = new BukkitPlatformSender(sender);
Invocation<CommandSender> invocation = new Invocation<>(sender, platformSender, commandRoute.getName(), alias, input);

return CompletableFuture.completedFuture(this.suggestionHook.suggest(new Invocation<>(sender, platformSender, commandRoute.getName(), alias, input), input)
.asMultiLevelList());
return CompletableFuture.completedFuture(this.suggestionHook.suggest(invocation, input).getSuggestions()); // TODO Run suggestion asynchronously inside LiteCommands platform
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import dev.rollczi.litecommands.input.raw.RawCommand;
import dev.rollczi.litecommands.reflect.LiteCommandsReflectException;
import dev.rollczi.litecommands.scheduler.Scheduler;
import dev.rollczi.litecommands.suggestion.Suggestion;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.Nullable;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
Expand Down Expand Up @@ -48,7 +49,7 @@ void close() {
}

@Nullable
protected List<String> callListener(CommandSender sender, String buffer) {
protected Set<Suggestion> callListener(CommandSender sender, String buffer) {
if (!buffer.startsWith(RawCommand.COMMAND_SLASH)) {
return null;
}
Expand All @@ -72,11 +73,17 @@ protected List<String> callListener(CommandSender sender, String buffer) {

static TabComplete create(Scheduler scheduler, Plugin plugin) {
try {
Class.forName("com.destroystokyo.paper.event.server.AsyncTabCompleteEvent");
Class.forName("com.destroystokyo.paper.event.server.AsyncTabCompleteEvent$Completion");
return new TabCompletePaperAsync(plugin);
}
catch (ClassNotFoundException ignored) {}

try {
Class.forName("com.destroystokyo.paper.event.server.AsyncTabCompleteEvent");
return new TabCompletePaper1_15Async(plugin);
}
catch (ClassNotFoundException ignored) {}

try {
Class.forName("com.comphenix.protocol.ProtocolLibrary");
Class.forName("org.bukkit.craftbukkit.libs.jline.console.ConsoleReader");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package dev.rollczi.litecommands.bukkit;

import dev.rollczi.litecommands.LiteCommandsException;
import dev.rollczi.litecommands.reflect.ReflectUtil;
import dev.rollczi.litecommands.suggestion.Suggestion;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.bukkit.command.CommandSender;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;

/**
* This class is responsible for handling tab completion for Paper 1.12-1.15 versions.
*/
class TabCompletePaper1_15Async extends TabComplete implements Listener {

public TabCompletePaper1_15Async(Plugin plugin) {
PluginManager pluginManager = plugin.getServer().getPluginManager();
pluginManager.registerEvent(PaperAsyncTabCompleteEvent.TAB_COMPLETE_CLASS, this, EventPriority.HIGHEST, (listener, event) -> executeListener(event), plugin);
}

private void executeListener(Event event) {
PaperAsyncTabCompleteEvent tabCompleteEvent = new PaperAsyncTabCompleteEvent(event);
Set<Suggestion> result = this.callListener(tabCompleteEvent.getSender(), tabCompleteEvent.getBuffer());

if (result == null) {
return;
}

tabCompleteEvent.setCompletions(result.stream()
.map(suggestion -> suggestion.multilevel())
.collect(Collectors.toList())
);
}

public void close() {
super.close();
HandlerList.unregisterAll(this);
}

static class PaperAsyncTabCompleteEvent {

private static final String TAB_COMPLETE_CLASS_NAME = "com.destroystokyo.paper.event.server.AsyncTabCompleteEvent";

static final Class<? extends Event> TAB_COMPLETE_CLASS;
static final Method GET_COMMAND_SENDER_METHOD;
static final Method GET_BUFFER_METHOD;
static final Method SET_COMPLETIONS_METHOD;

static {
TAB_COMPLETE_CLASS = ReflectUtil.getClass(TAB_COMPLETE_CLASS_NAME);
GET_COMMAND_SENDER_METHOD = ReflectUtil.getMethod(TAB_COMPLETE_CLASS, "getSender");
GET_BUFFER_METHOD = ReflectUtil.getMethod(TAB_COMPLETE_CLASS, "getBuffer");
SET_COMPLETIONS_METHOD = ReflectUtil.getMethod(TAB_COMPLETE_CLASS, "setCompletions", List.class);
}

private final Object event;

PaperAsyncTabCompleteEvent(Object event) {
if (!ReflectUtil.instanceOf(event, TAB_COMPLETE_CLASS)) {
throw new LiteCommandsException("Event is not instance of " + TAB_COMPLETE_CLASS_NAME);
}

this.event = event;
}

public CommandSender getSender() {
return ReflectUtil.invokeMethod(GET_COMMAND_SENDER_METHOD, event);
}

public String getBuffer() {
return ReflectUtil.invokeMethod(GET_BUFFER_METHOD, event);
}

public void setCompletions(List<String> completions) {
ReflectUtil.invokeMethod(SET_COMPLETIONS_METHOD, event, completions);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import dev.rollczi.litecommands.LiteCommandsException;
import dev.rollczi.litecommands.reflect.ReflectUtil;
import dev.rollczi.litecommands.suggestion.Suggestion;
import org.bukkit.command.CommandSender;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
Expand All @@ -12,6 +13,8 @@

import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

class TabCompletePaperAsync extends TabComplete implements Listener {

Expand All @@ -22,13 +25,13 @@ public TabCompletePaperAsync(Plugin plugin) {

private void executeListener(Event event) {
PaperAsyncTabCompleteEvent tabCompleteEvent = new PaperAsyncTabCompleteEvent(event);
List<String> result = this.callListener(tabCompleteEvent.getSender(), tabCompleteEvent.getBuffer());
Set<Suggestion> result = this.callListener(tabCompleteEvent.getSender(), tabCompleteEvent.getBuffer());

if (result == null) {
return;
}

tabCompleteEvent.setCompletions(result);
tabCompleteEvent.completions(result);
}

public void close() {
Expand All @@ -39,17 +42,31 @@ public void close() {
static class PaperAsyncTabCompleteEvent {

private static final String TAB_COMPLETE_CLASS_NAME = "com.destroystokyo.paper.event.server.AsyncTabCompleteEvent";
private static final String TAB_COMPLETION_CLASS_NAME = "com.destroystokyo.paper.event.server.AsyncTabCompleteEvent$Completion";
private static final String COMPONENT_CLASS_NAME = "net{}kyori{}adventure{}text{}Component";// Ignore relocation

static final Class<? extends Event> TAB_COMPLETE_CLASS;
static final Class<?> TAB_COMPLETION_CLASS;
static final Class<?> COMPONENT_CLASS;
static final Method GET_COMMAND_SENDER_METHOD;
static final Method GET_BUFFER_METHOD;
static final Method SET_COMPLETIONS_METHOD;

static final Method CREATE_COMPLETION_TOOLTIP;
static final Method COMPLETIONS_METHOD;
static final Method COMPONENT_TEXT_METHOD;

static {
TAB_COMPLETE_CLASS = ReflectUtil.getClass(TAB_COMPLETE_CLASS_NAME);
GET_COMMAND_SENDER_METHOD = ReflectUtil.getMethod(TAB_COMPLETE_CLASS, "getSender");
GET_BUFFER_METHOD = ReflectUtil.getMethod(TAB_COMPLETE_CLASS, "getBuffer");
SET_COMPLETIONS_METHOD = ReflectUtil.getMethod(TAB_COMPLETE_CLASS, "setCompletions", List.class);

TAB_COMPLETION_CLASS = ReflectUtil.getClass(TAB_COMPLETION_CLASS_NAME);
COMPONENT_CLASS = ReflectUtil.getClass(COMPONENT_CLASS_NAME.replace("{}", "."));
CREATE_COMPLETION_TOOLTIP = ReflectUtil.getMethod(TAB_COMPLETION_CLASS, "completion", String.class, COMPONENT_CLASS);
COMPLETIONS_METHOD = ReflectUtil.getMethod(TAB_COMPLETE_CLASS, "completions", List.class);
COMPONENT_TEXT_METHOD = ReflectUtil.getMethod(COMPONENT_CLASS, "text", String.class);
}

private final Object event;
Expand All @@ -74,5 +91,23 @@ public void setCompletions(List<String> completions) {
ReflectUtil.invokeMethod(SET_COMPLETIONS_METHOD, event, completions);
}

public void completions(Set<Suggestion> suggestions) {
List<Object> completions = suggestions.stream()
.map(suggestion -> ReflectUtil.invokeMethod(CREATE_COMPLETION_TOOLTIP, null, suggestion.multilevel(), toComponent(suggestion.tooltip())))
.collect(Collectors.toList());

ReflectUtil.invokeMethod(COMPLETIONS_METHOD, event, completions);
}

public Object toComponent(String text) {
if (text == null || text.isEmpty()) {
return null;
}

// TODO ComponentSerializer support for Adventure
// There we can have problems with relocation for adventure platform
// Maybe we should use json format?
return ReflectUtil.invokeMethod(COMPONENT_TEXT_METHOD, null, text);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import dev.rollczi.litecommands.reflect.ReflectUtil;
import dev.rollczi.litecommands.scheduler.Scheduler;
import dev.rollczi.litecommands.scheduler.SchedulerPoll;
import dev.rollczi.litecommands.suggestion.Suggestion;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import org.bukkit.craftbukkit.libs.jline.console.ConsoleReader;
Expand All @@ -20,11 +21,11 @@

import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

class TabCompleteProtocolLibAsync extends TabCompleteSync {

Expand Down Expand Up @@ -88,15 +89,17 @@ private void handlePacket(PacketEvent event) {
event.setCancelled(true);
scheduler.run(SchedulerPoll.SUGGESTER, () -> {
try {
List<String> list = command.suggest(player, commandName, rawCommand.getArgs().toArray(new String[0]))
Set<Suggestion> suggestions = command.suggest(player, commandName, rawCommand.getArgs().toArray(new String[0]))
.get(15, TimeUnit.SECONDS);

if (list == null) {
if (suggestions == null) {
return;
}
String[] list = suggestions.stream().map(c -> c.multilevel())
.toArray(String[]::new);

PacketContainer packet = MANAGER.createPacket(PacketType.Play.Server.TAB_COMPLETE);
packet.getStringArrays().write(0, list.toArray(new String[0]));
packet.getStringArrays().write(0, list);

MANAGER.sendServerPacket(player, packet);
}
Expand Down Expand Up @@ -129,22 +132,23 @@ public ProtocolLibConsoleTabConsoleCompleter(CommandSender consoleSender, Comple
@Override
public int complete(String buffer, int cursor, List<CharSequence> candidates) {
int completed = completer.complete(buffer, cursor, candidates);
List<String> result = callListener(consoleSender, buffer);
Set<Suggestion> result = callListener(consoleSender, buffer);

if (result == null && cursor == completed) {
return completed;
}

if (result != null) {
candidates.addAll(result);
for (Suggestion suggestion : result) {
candidates.add(suggestion.multilevel());
}
}

int lastSpace = buffer.lastIndexOf(' ');

if (lastSpace == -1) {
return cursor - buffer.length();
}
else {
} else {
return cursor - (buffer.length() - lastSpace - 1);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
import dev.rollczi.litecommands.bukkit.LiteBukkitMessages;
import dev.rollczi.litecommands.invocation.Invocation;
import dev.rollczi.litecommands.message.MessageRegistry;
import dev.rollczi.litecommands.suggestion.Suggestion;
import dev.rollczi.litecommands.suggestion.SuggestionContext;
import dev.rollczi.litecommands.suggestion.SuggestionResult;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

import java.util.stream.Collectors;

public class PlayerArgument extends ArgumentResolver<CommandSender, Player> {

private final Server server;
Expand All @@ -36,8 +39,7 @@ protected ParseResult<Player> parse(Invocation<CommandSender> invocation, Argume
@Override
public SuggestionResult suggest(Invocation<CommandSender> invocation, Argument<Player> argument, SuggestionContext context) {
return server.getOnlinePlayers().stream()
.map(Player::getName)
.collect(SuggestionResult.collector());
.collect(SuggestionResult.collector(player -> player.getName(), player -> player.getUniqueId().toString()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
import dev.rollczi.litecommands.annotations.command.Command;
import dev.rollczi.litecommands.annotations.execute.Execute;
import dev.rollczi.litecommands.annotations.join.Join;
import dev.rollczi.litecommands.argument.Argument;
import dev.rollczi.litecommands.chatgpt.annotation.ChatGpt;
import dev.rollczi.litecommands.join.JoinProfile;
import dev.rollczi.litecommands.programmatic.LiteCommand;
import dev.rollczi.litecommands.scheduler.SchedulerSameThreadImpl;
import dev.rollczi.litecommands.unit.annotations.LiteTestSpec;
Expand Down
Loading

0 comments on commit f621def

Please sign in to comment.