Tutorial Using TabComplete for configuration node navigation

Discussion in 'Resources' started by lycano, Mar 10, 2015.

Thread Status:
Not open for further replies.
  1. Offline

    lycano

    Hi,

    having looked over this nice little tutorial https://bukkit.org/threads/easy-no-api-setting-up-custom-tab-completion.299956/ i thought i could leave this little extended tutorial here for you hoping it will be of any use to you.

    I have played around with tabCompletion for hours to understand what it can do and what not.

    What is TabComplete?

    There are two different TabCompletion modes in the client. TabComplete for chat and TabComplete for commands.

    TabComplete for chat does make use of the event PlayerChatTabCompleteEvent the later is bound to Command class and needs to be registered via setTabCompleter(TabCompleter completer) call on getCommand(String command). This is described in nice tutorial TheHandfish wrote.

    TabComplete for Commands is called whenever you use a slash at start and type a command. However you can only attach to your command not to bukkits tabcomplete when using /<tab> at least there is not an easy way.

    Normally when using TabComplete in a command lets say /yourcommand config <node> and you press tab after config you would get a playerlist since tabcomplete does tabcomplete online players by default. To make use of this you need to make your custom TabCompleter as described in the link above.

    What is possible with TabCompletion?

    • By Using TabComplete you can really improve the quality of your plugin and speed up things
    • Browse quickly through your commands and subcommands
    • Make a Configuration-Node browser
    • And much more
    What is not possible with TabCompleter

    TabCompletion is designed to complete a word from a list of strings and only one list. It expects that you type a portion of a playername and then gives back a playername list. It is not designed to complete different results for one argument.

    Example:
    You type in filt<tab> you get a list of possible values [filter, config, permissions, debug] now you do
    filter<dot><tab>. You get a new list [alpha, beta, cappa, delta].

    filter<dot> is replaced by the first hit in that list client side. filter<dot> changes to a.

    For this tutorial we want to get back filter.a or filter.b and only complete the last part. Is this possible? At first I thought no since whenever i tried to prefix the list with the previous node the list was not displayed on the client. Furthermore i got the first result from the list previously displayed back in this case filter.alpha but never i got filter.delta when i typed filter.de<tab>.

    Why? Well whenever you press tab you trigger tabcomplete on the server. A list is generated and the result is send back to the client. The client does update the order of the list only if the result matches! If the list changed along the line the list on the client side will not be updated and the first entry in that list on the client will be shown.

    So we need to remember: In order to complete the command that is part of the list correctly and get a result back that would match your partial command we need to only send the matching command back since when you send a full sorted list it will not update the list when the list is populated because there is no difference. I.e. having the following list:

    : filter.<tab>
    filter.allowed, filter.blank-name, filter.commands, filter.disallowed, filter.min-length
    : filter.co<tab>
    this will not give you filter.commands back. You will infact get filter.allowed back since the list is populated and all further tab pressing will be client side not server side (entry count is the same).

    So how do we make a ConfigurationNodeBrowser if this is not possible?

    We could use arguments and use a tabcompleter for each argument e.g. filter<space>node<tab> but that would be ugly at least in my opionion. I wanted to have the <dot> as divider and only update the last part.

    I found a way to do this by using a little trick to force the client to show the correct command. The user does not notice it since its part of the normal browsing.

    How?

    Back to our little example:

    : filter.<tab>
    filter., filter.allowed, filter.blank-name, filter.commands, filter.disallowed, filter.min-length
    (result: filter. cause its the first in the list)
    type in: filter.co<tab>

    The result we send back now is filter.command. Since its not the same the client will display this entry but does not update the list because it is already in the list (no change done)

    And voila filter.commands will be displayed. I also added an identifier for nodes that have childrens. Since using colors would result in having the color code number in your command this was the solution i came up with.

    Lets go to the next part where i show you how this is done programatically.

    How to write a ConfigurationNode Browser with TabComplete

    The Example shown below expects that you have a single command for node browsing lets say:
    Code:
    /config <node>
    
    First we need a comperator. For this example we use the name TabCompleteComperator
    TabCompleteComperator (open)

    Code:
    import java.util.Comparator;
    
    public class TabCompleteComperator implements Comparator<String> {
    
      private final String keyword;
    
      public TabCompleteComperator(String keyword) {
          this.keyword = keyword;
      }
    
      @Override
      public int compare(String o1, String o2) {
          if (o1.startsWith(keyword)) {
              return o2.startsWith(keyword) ? o1.compareTo(o2) : -1;
          } else {
              return o2.startsWith(keyword) ? 1 : o1.compareTo(o2);
          }
      }
    }
    

    ConfigCommandTabComplete (open)

    Code:
    public class ConfigCommandTabComplete implements TabCompleter {
    
      private Configuration config;
      private String command;
      private int nodeLevel = 0;
      private String[] nodeList;
      private String delimiter = ".";
      private String splitDelimiter = "\\.";
    
      public ConfigCommandTabComplete(Configuration config) {
          this.config = config;
      }
    
      /**
      * Requests a list of possible completions for a command argument.
      *
      * @param sender  Source of the command
      * @param command Command which was executed
      * @param alias  The alias used
      * @param args  The arguments passed to the command, including final
      *  partial argument to be completed and command label
      * @return A List of possible completions for the final argument, or null
      * to default to the command executor
      */
      @Override
      public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
          try {
              this.command = args[0].replaceFirst("\\+", "").toLowerCase();
              String node;
    
              this.nodeList = this.command.split(this.splitDelimiter);
              this.nodeLevel = this.nodeList.length - 1;
    
              if (this.command.endsWith(this.delimiter))
                  this.nodeLevel++;
    
              Set<String> keys;
              if (this.nodeLevel > 0) {
                  node = this.basePath(this.command);
                  keys = this.config.getConfigurationSection(node).getKeys(false);
    
                  Set<String> prefixedKeys = new HashSet<String>();
                  for (String key: keys) {
                      prefixedKeys.add(this.basePath(this.command) + this.delimiter + key);
                  }
                  keys = prefixedKeys;
              } else {
                  keys = this.config.getKeys(false);
              }
    
              this.nodeList = keys.toArray(new String[keys.size()]);
              Arrays.sort(this.nodeList, new TabCompleteComperator(this.command));
    
              if (this.nodeLevel > 0) {
                  if (this.command.endsWith(".")) {
                      List<String> modifiedList = new ArrayList<String>(Arrays.asList(this.command));
                      modifiedList.addAll(new ArrayList<String>(Arrays.asList(this.nodeList)));
    
                      this.translateChildNodes(modifiedList);
    
                      this.nodeList = modifiedList.toArray(new String[modifiedList.size()]);
                  } else if (!(this.command.equals(this.nodeList[0]))) {
                      String tmp = this.nodeList[0];
                      this.nodeList = new String[1];
                      this.nodeList[0] = this.translateChildNode(tmp);
                  }
              }
    
              return new ArrayList<String>(Arrays.asList(this.nodeList));
            } catch (Exception e) {
            // place your logging here for debugging
            }
    
            return new ArrayList<String>();
      }
    
      private String basePath(String path) {
          return (path.lastIndexOf(this.delimiter) > 0) ? path.substring(0, path.lastIndexOf(this.delimiter)) : path;
      }
    
      private String translateChildNode(String node) {
          List<String> childNode = new ArrayList<String>(Arrays.asList(node));
          this.translateChildNodes(childNode);
          return childNode.get(0);
      }
    
      private void translateChildNodes(List<String> nodeList) {
          ConfigurationSection configurationSection;
          for (int i=0; i < nodeList.size(); i++) {
              String path = nodeList.get(i);
              configurationSection = this.config.getConfigurationSection(path);
              if (configurationSection != null) {
                  nodeList.set(i, String.format("%s%s", "+", path));
              }
          }
      }
    


    If your command expect it to be the second argument like you have /maincommand config <node> then just change

    Code:
    this.command = args[0].replaceFirst("\\+", "").toLowerCase();
    
    into

    Code:
    this.command = args[1].replaceFirst("\\+", "").toLowerCase();
    
    Why do we need a comperator?

    In order to sort the List we create we need a rule how it should be sorted. Thats why we need a comperator . When you type your command Arrays.sort will sort the list ascending where it finds a match using startsWith.

    This is needed because we want to have the best matching result at first place.

    A word to NodeLevel?

    Configuration is divided into sections. In order to know at what level we are in the nodepath this is needed to know what we have to query since we will most likely dont get a result when we do "filter.co" in this case we need to fall back one level and query for filter without doing deep seek. Check the Documentation for further details.

    Whenever a deeper node is queried the list will update and give you new options as shown below in section Illustration. If the node has a subsection then a plus sign will be prefixed to the node. Simply append a dot at the end and press TAB again to get new options.

    Illustration

    demonstration.gif


    Conclusion


    We have seen how TabCompleter can make your live easier when using your plugin. You can use these classes to make a custom player filter. Doing so is far more easier than implementing this since you do not have to worry about subcommands.

    Feel free to like and share and if you can improve this class please create a gist on github and let me know.

    Thanks for reading!
     
Thread Status:
Not open for further replies.

Share This Page