Skip to content

Commit

Permalink
Merge pull request #150 from SpigotBasics/multi-arg-tab-complete
Browse files Browse the repository at this point in the history
Working TabComplete for multi-length and greedy args
  • Loading branch information
mfnalex authored Feb 20, 2024
2 parents 2551273 + 13c7dea commit ee19079
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.github.spigotbasics.common.Either
import com.github.spigotbasics.core.Basics
import com.github.spigotbasics.core.command.parsed.arguments.CommandArgument
import com.github.spigotbasics.core.command.parsed.context.CommandContext
import com.github.spigotbasics.core.extensions.lastOrEmpty
import com.github.spigotbasics.core.logger.BasicsLoggerFactory
import com.github.spigotbasics.core.messages.Message
import org.bukkit.command.CommandSender
Expand Down Expand Up @@ -216,36 +215,109 @@ class ArgumentPath<T : CommandContext>(
sender: CommandSender,
input: List<String>,
): Boolean {
logger.debug(200, "TabComplete matchesStart: input: $input @ $this")
if (input.size > arguments.size) {
logger.debug(200, " input.size > arguments.size")
return false
}
input.forEachIndexed { index, s ->
logger.debug(200, " Checking index $index, s: $s")
if (index == input.size - 1) {
logger.debug(200, " Last argument, skipping")
return@forEachIndexed
} // Last argument is still typing
val arg = arguments[index].second
val parsed = arg.parse(sender, s)
if (parsed == null) {
logger.debug(200, " parsed == null")
if (!isCorrectSender(sender) || !hasPermission(sender)) return false

var accumulatedInputCount = 0
for ((index, pair) in arguments.withIndex()) {
val (_, argument) = pair
val isLastArgument = index == arguments.size - 1

// Calculate the expected count of inputs for this argument
val expectedCount = if (argument.greedy) input.size - accumulatedInputCount else argument.length
val availableInputs = input.drop(accumulatedInputCount).take(expectedCount)
val argumentInput = availableInputs.joinToString(" ")

// Update the accumulated input count for the next argument
accumulatedInputCount += availableInputs.size

// If it's not the last argument and parsing fails, return false
if (!isLastArgument && argument.parse(sender, argumentInput) == null) {
return false
}

// If it's the last argument, it's okay if parse returns null (user might still be typing)
if (isLastArgument) {
// If there's enough input for the argument to potentially parse, return true (assume user might complete it)
// If argument is greedy, always return true since we're accommodating partial input
if (argument.greedy || argument.parse(sender, argumentInput) != null) {
return true
} else {
// For non-greedy last argument, it's okay if parsing fails due to partial input
return availableInputs.size >= argument.length
}
}
}
logger.debug(200, " All arguments parsed, this path matches the input")

// If the loop completes without returning, all arguments except possibly the last have parsed successfully
return true
}

// fun matchesStart(
// sender: CommandSender,
// input: List<String>,
// ): Boolean {
// if (!isCorrectSender(sender) || !hasPermission(sender)) return false
//
// var currentIndex = 0
// arguments.forEachIndexed { index, (_, argument) ->
// if (!argument.greedy) {
// // Check if current argument can be fully covered by remaining input
// if (currentIndex + argument.length > input.size) {
// // If this is the last argument or a greedy argument is next and the user is still typing it, partial input is okay
// if (index == arguments.size - 1 || (arguments.getOrNull(index + 1)?.second?.greedy ?: false)) {
// return true // Accept partial input for the last non-greedy argument or before a greedy one
// }
// return false // Not enough input for this non-greedy argument
// }
// currentIndex += argument.length
// } else {
// // For a greedy argument, ensure at least its minimum length is available
// if (currentIndex + argument.length > input.size) {
// // Allow partial input for greedy arguments if it's the last argument
// return index == arguments.size - 1
// }
// // Greedy argument consumes the rest; no need to check further
// return true
// }
// }
//
// // If all non-greedy arguments up to the last one were satisfied with correct length, or a greedy argument had enough input
// return true
// }

fun tabComplete(
sender: CommandSender,
args: List<String>,
input: List<String>,
): List<String> {
if (args.isEmpty() || args.size > arguments.size) return emptyList()
if (!isCorrectSender(sender) || !hasPermission(sender)) return emptyList()

// Identify the argument the user is currently completing
val currentIndex = input.size - 1
var accumulatedIndex = 0

arguments.forEachIndexed { index, (argName, argument) ->
val isLastArgument = index == arguments.size - 1
val isGreedyArgument = argument.greedy

// Calculate the start and end index for the current argument's input
val startIndex = accumulatedIndex
var endIndex = accumulatedIndex + argument.length

// If the argument is greedy, or we are at the last argument, adjust endIndex to include all remaining input
if (isGreedyArgument || isLastArgument) endIndex = input.size

// Update accumulatedIndex for the next iteration
accumulatedIndex += argument.length

// Check if the current argument is the one being completed
if (currentIndex >= startIndex && currentIndex < endIndex) {
// For greedy and last arguments, include all remaining input; otherwise, limit to the argument's length
val relevantInput = if (currentIndex < input.size) input.subList(startIndex, endIndex).joinToString(" ") else ""
return argument.tabComplete(sender, relevantInput)
}
}

val currentArgIndex = args.size - 1
return arguments[currentArgIndex].second.tabComplete(sender, args.lastOrEmpty())
return emptyList()
}

fun isCorrectSender(sender: CommandSender): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import org.bukkit.command.CommandSender
import org.bukkit.entity.Entity
import org.bukkit.entity.Player

abstract class SelectorEntityArgBase<T>(name: String) : CommandArgument<T>(name) {
abstract class SelectorEntityArgBase<T>(
name: String,
private val allowMultiple: Boolean,
private val allowEntities: Boolean,
) : CommandArgument<T>(name) {
protected enum class ErrorType {
NOT_FOUND,
NO_PERMISSION_SELECTORS,
Expand All @@ -27,21 +31,39 @@ abstract class SelectorEntityArgBase<T>(name: String) : CommandArgument<T>(name)
sender: CommandSender,
typing: String,
): List<String> {
val playerNames = getPlayerNames(sender)
val selectors = getSelectors(sender)
return (playerNames + selectors).partialMatches(typing)
}

private fun getPlayerNames(sender: CommandSender): List<String> {
return Bukkit.getOnlinePlayers().filter {
if (sender is Player) {
sender.canSee(it)
} else {
true
}
}.map { it.name }.partialMatches(typing)
}.map { it.name }
}

private fun getSelectors(sender: CommandSender): List<String> {
if (!sender.hasPermission(selectorPermission)) {
return emptyList()
}
val list = mutableListOf("@p", "@r", "@s")
if (allowEntities && allowMultiple) {
list.add("@e")
}
if (allowMultiple) {
list.add("@a")
}
return list
}

// TODO: Additional canSee check for all matched players?
protected fun get(
sender: CommandSender,
value: String,
allowMultiple: Boolean,
allowEntities: Boolean,
): Either<ErrorType, List<Entity>> {
val onePlayer = Bukkit.getPlayer(value)
if (onePlayer != null) {
Expand Down Expand Up @@ -75,10 +97,8 @@ abstract class SelectorEntityArgBase<T>(name: String) : CommandArgument<T>(name)
protected fun errorMessage0(
sender: CommandSender,
value: String,
allowMultiple: Boolean,
allowEntities: Boolean,
): Message {
return when (get(sender, value, allowMultiple, allowEntities).leftOrNull()) {
return when (get(sender, value).leftOrNull()) {
ErrorType.NOT_FOUND -> Basics.messages.playerNotFound(value)
ErrorType.NOT_FOUND_ENTITY -> Basics.messages.selectorMatchesNoEntities(name, value)
ErrorType.NO_PERMISSION_SELECTORS -> Basics.messages.noPermission(selectorPermission)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import com.github.spigotbasics.core.messages.Message
import org.bukkit.command.CommandSender
import org.bukkit.entity.Entity

class SelectorMultiEntityArg(name: String) : SelectorEntityArgBase<List<Entity>>(name) {
class SelectorMultiEntityArg(name: String) : SelectorEntityArgBase<List<Entity>>(name, allowMultiple = true, allowEntities = true) {
override fun parse(
sender: CommandSender,
value: String,
): List<Entity>? {
return get(sender, value, allowMultiple = true, allowEntities = true).fold(
return get(sender, value).fold(
{ _ -> null },
{ it },
)
Expand All @@ -19,6 +19,6 @@ class SelectorMultiEntityArg(name: String) : SelectorEntityArgBase<List<Entity>>
sender: CommandSender,
value: String,
): Message {
return errorMessage0(sender, value, allowMultiple = true, allowEntities = true)
return errorMessage0(sender, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import com.github.spigotbasics.core.messages.Message
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player

class SelectorMultiPlayerArg(name: String) : SelectorEntityArgBase<List<Player>>(name) {
class SelectorMultiPlayerArg(name: String) : SelectorEntityArgBase<List<Player>>(name, allowMultiple = true, allowEntities = false) {
override fun parse(
sender: CommandSender,
value: String,
): List<Player>? {
return get(sender, value, allowMultiple = true, allowEntities = false).fold(
return get(sender, value).fold(
{ _ -> null },
{ it },
)?.map { it as Player }
Expand All @@ -19,6 +19,6 @@ class SelectorMultiPlayerArg(name: String) : SelectorEntityArgBase<List<Player>>
sender: CommandSender,
value: String,
): Message {
return errorMessage0(sender, value, allowMultiple = true, allowEntities = false)
return errorMessage0(sender, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import com.github.spigotbasics.core.messages.Message
import org.bukkit.command.CommandSender
import org.bukkit.entity.Entity

class SelectorSingleEntityArg(name: String) : SelectorEntityArgBase<Entity>(name) {
class SelectorSingleEntityArg(name: String) : SelectorEntityArgBase<Entity>(name, allowMultiple = false, allowEntities = true) {
override fun parse(
sender: CommandSender,
value: String,
): Entity? {
return get(sender, value, allowMultiple = false, allowEntities = true).rightOrNull()?.singleOrNull()
return get(sender, value).rightOrNull()?.singleOrNull()
}

override fun errorMessage(
sender: CommandSender,
value: String,
): Message {
return errorMessage0(sender, value, allowMultiple = false, allowEntities = true)
return errorMessage0(sender, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import com.github.spigotbasics.core.messages.Message
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player

class SelectorSinglePlayerArg(name: String) : SelectorEntityArgBase<Player>(name) {
class SelectorSinglePlayerArg(name: String) : SelectorEntityArgBase<Player>(name, allowMultiple = false, allowEntities = false) {
override fun parse(
sender: CommandSender,
value: String,
): Player? {
return get(sender, value, allowMultiple = false, allowEntities = false).rightOrNull()?.singleOrNull() as Player?
return get(sender, value).rightOrNull()?.singleOrNull() as Player?
}

override fun errorMessage(
sender: CommandSender,
value: String,
): Message {
return errorMessage0(sender, value, allowMultiple = false, allowEntities = false)
return errorMessage0(sender, value)
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.github.spigotbasics.core.command.parsed.arguments

import com.github.spigotbasics.core.extensions.lastOrEmpty
import com.github.spigotbasics.core.extensions.partialMatches
import com.github.spigotbasics.core.logger.BasicsLoggerFactory
import com.github.spigotbasics.core.model.TripleContextCoordinates
import org.bukkit.command.CommandSender

class TripleContextCoordinatesArg(name: String) : CommandArgument<TripleContextCoordinates>(name) {
companion object {
val logger = BasicsLoggerFactory.getCoreLogger(TripleContextCoordinatesArg::class)
}

override fun parse(
sender: CommandSender,
value: String,
Expand All @@ -19,10 +26,16 @@ class TripleContextCoordinatesArg(name: String) : CommandArgument<TripleContextC
sender: CommandSender,
typing: String,
): List<String> {
// println(typing)
// TODO: Add selectors and ~ ~~ for tabcomplete if has permission
// TODO: That requires passing the concat-ed string to the tabComplete method
return super.tabComplete(sender, typing)
logger.debug(1, typing)

val split = typing.split(" ")
if (split.size > 5) return emptyList()
for (previous in split.dropLast(1)) {
if (!TripleContextCoordinates.isValidSinglePart(previous)) {
return emptyList()
}
}
return listOf("~", "~~").partialMatches(split.lastOrEmpty())
}

override val greedy = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,11 @@ data class TripleContextCoordinates(
Pair(string.toDouble(), Relativity.ABSOLUTE)
}
}

private val validSinglePartRegex = Regex("^(~|~~)?-?\\d*(\\.\\d+)?$")

fun isValidSinglePart(string: String): Boolean {
return string.matches(validSinglePartRegex)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.spigotbasics.modules.basicscore

import com.github.spigotbasics.core.command.parsed.arguments.IntRangeArg
import com.github.spigotbasics.core.command.parsed.arguments.TripleContextCoordinatesArg
import com.github.spigotbasics.core.module.AbstractBasicsModule
import com.github.spigotbasics.core.module.loader.ModuleInstantiationContext
import com.github.spigotbasics.modules.basicscore.commands.CreateGiveCommand
Expand Down Expand Up @@ -141,5 +142,14 @@ class BasicsCoreModule(context: ModuleInstantiationContext) : AbstractBasicsModu
}
}
.register()

commandFactory.parsedCommandBuilder("tabtest", debugPermission)
.mapContext {
path {
arguments {
named("test", TripleContextCoordinatesArg("Test"))
}
}
}.executor(TabTestCommand()).register()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.spigotbasics.modules.basicscore

import com.github.spigotbasics.core.command.parsed.CommandContextExecutor
import com.github.spigotbasics.core.command.parsed.context.MapContext
import org.bukkit.command.CommandSender

class TabTestCommand :
CommandContextExecutor<MapContext> {
override fun execute(
sender: CommandSender,
context: MapContext,
) {
sender.sendMessage("TabTestCommand executed: $context")
}
}

0 comments on commit ee19079

Please sign in to comment.