cli: auto completions

This commit is contained in:
Bixilon 2022-05-29 19:26:22 +02:00
parent 805edb8590
commit 7191fdcb76
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
9 changed files with 92 additions and 15 deletions

View File

@ -16,6 +16,6 @@ package de.bixilon.minosoft.commands.errors.literal
import de.bixilon.minosoft.commands.errors.ReaderError import de.bixilon.minosoft.commands.errors.ReaderError
import de.bixilon.minosoft.commands.util.CommandReader import de.bixilon.minosoft.commands.util.CommandReader
class TrailingTextArgument( class TrailingTextError(
reader: CommandReader, reader: CommandReader,
) : ReaderError(reader, reader.pointer, reader.length) ) : ReaderError(reader, reader.pointer, reader.length)

View File

@ -14,7 +14,7 @@
package de.bixilon.minosoft.commands.nodes package de.bixilon.minosoft.commands.nodes
import de.bixilon.minosoft.commands.errors.DeadEndError import de.bixilon.minosoft.commands.errors.DeadEndError
import de.bixilon.minosoft.commands.errors.literal.TrailingTextArgument import de.bixilon.minosoft.commands.errors.literal.TrailingTextError
import de.bixilon.minosoft.commands.stack.CommandStack import de.bixilon.minosoft.commands.stack.CommandStack
import de.bixilon.minosoft.commands.util.CommandReader import de.bixilon.minosoft.commands.util.CommandReader
@ -47,7 +47,7 @@ abstract class CommandNode(
try { try {
executeChild(child, reader, stack) executeChild(child, reader, stack)
if (reader.canPeek()) { if (reader.canPeek()) {
throw TrailingTextArgument(reader) throw TrailingTextError(reader)
} }
return return
} catch (error: Throwable) { } catch (error: Throwable) {
@ -78,7 +78,7 @@ abstract class CommandNode(
try { try {
val childSuggestions = child.getSuggestions(reader, stack) val childSuggestions = child.getSuggestions(reader, stack)
if (reader.canPeek()) { if (reader.canPeek()) {
throw TrailingTextArgument(reader) throw TrailingTextError(reader)
} }
parserSucceeds++ parserSucceeds++
@ -115,7 +115,7 @@ abstract class CommandNode(
protected fun checkForDeadEnd(reader: CommandReader) { protected fun checkForDeadEnd(reader: CommandReader) {
if (children.isEmpty()) { if (children.isEmpty()) {
if (reader.canPeek()) { if (reader.canPeek()) {
throw TrailingTextArgument(reader) throw TrailingTextError(reader)
} else { } else {
throw DeadEndError(reader) throw DeadEndError(reader)
} }

View File

@ -98,7 +98,7 @@ open class TextInputElement(
textElement.unmark() textElement.unmark()
} }
private fun _set(value: String) { protected fun _set(value: String) {
val previous = _value.toString() val previous = _value.toString()
val next = _value.replace(0, _value.length, value) val next = _value.replace(0, _value.length, value)
_pointer = value.length _pointer = value.length

View File

@ -27,7 +27,7 @@ import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexOptions
import de.bixilon.minosoft.gui.rendering.system.window.KeyChangeTypes import de.bixilon.minosoft.gui.rendering.system.window.KeyChangeTypes
import de.bixilon.minosoft.util.KUtil import de.bixilon.minosoft.util.KUtil
class NodeSuggestionsElement(guiRenderer: GUIRenderer, position: Vec2i) : Popper(guiRenderer, position) { class NodeSuggestionsElement(guiRenderer: GUIRenderer, position: Vec2i, val inputElement: NodeTextInputElement) : Popper(guiRenderer, position) {
private var suggestionText = Array(MAX_SUGGESTIONS) { TextElement(guiRenderer, ChatComponent.EMPTY).apply { prefMaxSize = Vec2i(300, Font.TOTAL_CHAR_HEIGHT) } } private var suggestionText = Array(MAX_SUGGESTIONS) { TextElement(guiRenderer, ChatComponent.EMPTY).apply { prefMaxSize = Vec2i(300, Font.TOTAL_CHAR_HEIGHT) } }
private var textCount = 0 private var textCount = 0
private var offset = 0 private var offset = 0
@ -56,6 +56,15 @@ class NodeSuggestionsElement(guiRenderer: GUIRenderer, position: Vec2i) : Popper
field = value field = value
} }
val activeSuggestion: Any?
get() {
val suggestions = suggestions ?: return null
if (suggestions.isEmpty()) {
return null
}
return suggestions[offset]
}
override fun forceRender(offset: Vec2i, consumer: GUIVertexConsumer, options: GUIVertexOptions?) { override fun forceRender(offset: Vec2i, consumer: GUIVertexConsumer, options: GUIVertexOptions?) {
super.forceRender(offset, consumer, options) super.forceRender(offset, consumer, options)
@ -114,6 +123,9 @@ class NodeSuggestionsElement(guiRenderer: GUIRenderer, position: Vec2i) : Popper
if (type == KeyChangeTypes.RELEASE) { if (type == KeyChangeTypes.RELEASE) {
return super.onKey(key, type) return super.onKey(key, type)
} }
if (key == KeyCodes.KEY_ENTER || key == KeyCodes.KEY_KP_ENTER || key == KeyCodes.KEY_TAB) {
return applySuggestion()
}
val offset = when (key) { val offset = when (key) {
KeyCodes.KEY_UP -> -1 KeyCodes.KEY_UP -> -1
KeyCodes.KEY_PAGE_UP -> -5 KeyCodes.KEY_PAGE_UP -> -5
@ -134,6 +146,11 @@ class NodeSuggestionsElement(guiRenderer: GUIRenderer, position: Vec2i) : Popper
offset = 0 offset = 0
} }
fun applySuggestion(): Boolean {
inputElement.updateSuggestion(activeSuggestion ?: return false)
return true
}
private companion object { private companion object {
const val MAX_SUGGESTIONS = 10 const val MAX_SUGGESTIONS = 10
} }

View File

@ -28,6 +28,7 @@ import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexConsumer
import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexOptions import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexOptions
import de.bixilon.minosoft.gui.rendering.system.window.KeyChangeTypes import de.bixilon.minosoft.gui.rendering.system.window.KeyChangeTypes
import de.bixilon.minosoft.gui.rendering.util.vec.vec2.Vec2iUtil.EMPTY import de.bixilon.minosoft.gui.rendering.util.vec.vec2.Vec2iUtil.EMPTY
import de.bixilon.minosoft.util.KUtil
import de.bixilon.minosoft.util.logging.Log import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType import de.bixilon.minosoft.util.logging.LogMessageType
@ -48,7 +49,7 @@ class NodeTextInputElement(
) : TextInputElement(guiRenderer, value, maxLength, cursorStyles, editable, onChange, background, shadow, scale, cutAtSize, parent) { ) : TextInputElement(guiRenderer, value, maxLength, cursorStyles, editable, onChange, background, shadow, scale, cutAtSize, parent) {
private var showError = false private var showError = false
private val errorElement = NodeErrorElement(guiRenderer, Vec2i.EMPTY) private val errorElement = NodeErrorElement(guiRenderer, Vec2i.EMPTY)
private val suggestions = NodeSuggestionsElement(guiRenderer, Vec2i.EMPTY) private val suggestions = NodeSuggestionsElement(guiRenderer, Vec2i.EMPTY, this)
override fun forceRender(offset: Vec2i, consumer: GUIVertexConsumer, options: GUIVertexOptions?) { override fun forceRender(offset: Vec2i, consumer: GUIVertexConsumer, options: GUIVertexOptions?) {
@ -113,4 +114,10 @@ class NodeTextInputElement(
suggestions.onClose() suggestions.onClose()
errorElement.onClose() errorElement.onClose()
} }
fun updateSuggestion(suggestion: Any) {
val string = suggestion.toString()
_set(value + string.substring(KUtil.getOverlappingText(value, string), string.length))
forceApply()
}
} }

View File

@ -294,4 +294,22 @@ object KUtil {
return ret return ret
} }
fun getOverlappingText(start: String, end: String): Int {
var overlapping = 0
shift@ for (shift in 1..end.length) {
if (start.length < shift) {
break
}
for (i in 0 until shift) {
if (end.codePointAt(i) != start.codePointAt(start.length - (shift - i))) {
continue@shift
}
}
overlapping = shift
}
return overlapping
}
} }

View File

@ -16,7 +16,7 @@ package de.bixilon.minosoft.commands.nodes
import de.bixilon.minosoft.commands.errors.DeadEndError import de.bixilon.minosoft.commands.errors.DeadEndError
import de.bixilon.minosoft.commands.errors.literal.ExpectedLiteralArgument import de.bixilon.minosoft.commands.errors.literal.ExpectedLiteralArgument
import de.bixilon.minosoft.commands.errors.literal.InvalidLiteralArgumentError import de.bixilon.minosoft.commands.errors.literal.InvalidLiteralArgumentError
import de.bixilon.minosoft.commands.errors.literal.TrailingTextArgument import de.bixilon.minosoft.commands.errors.literal.TrailingTextError
import de.bixilon.minosoft.commands.errors.reader.ExpectedWhitespaceError import de.bixilon.minosoft.commands.errors.reader.ExpectedWhitespaceError
import de.bixilon.minosoft.commands.parser.brigadier.string.StringParseError import de.bixilon.minosoft.commands.parser.brigadier.string.StringParseError
import de.bixilon.minosoft.commands.parser.brigadier.string.StringParser import de.bixilon.minosoft.commands.parser.brigadier.string.StringParser
@ -90,7 +90,7 @@ internal class ExecutionChildReadingTest {
@Test @Test
fun testTrailingData() { fun testTrailingData() {
assertThrows<TrailingTextArgument> { (createCommand().execute(CommandReader("0_literal test"), CommandStack())) } assertThrows<TrailingTextError> { (createCommand().execute(CommandReader("0_literal test"), CommandStack())) }
} }
@Test @Test
@ -115,6 +115,6 @@ internal class ExecutionChildReadingTest {
@Test @Test
fun testTrailingTextEmptyRootNode() { fun testTrailingTextEmptyRootNode() {
assertThrows<TrailingTextArgument> { RootNode().execute(CommandReader("trailing"), CommandStack()) } assertThrows<TrailingTextError> { RootNode().execute(CommandReader("trailing"), CommandStack()) }
} }
} }

View File

@ -15,7 +15,7 @@ package de.bixilon.minosoft.commands.nodes
import de.bixilon.minosoft.commands.errors.DeadEndError import de.bixilon.minosoft.commands.errors.DeadEndError
import de.bixilon.minosoft.commands.errors.literal.InvalidLiteralArgumentError import de.bixilon.minosoft.commands.errors.literal.InvalidLiteralArgumentError
import de.bixilon.minosoft.commands.errors.literal.TrailingTextArgument import de.bixilon.minosoft.commands.errors.literal.TrailingTextError
import de.bixilon.minosoft.commands.errors.reader.ExpectedWhitespaceError import de.bixilon.minosoft.commands.errors.reader.ExpectedWhitespaceError
import de.bixilon.minosoft.commands.parser.brigadier.string.StringParseError import de.bixilon.minosoft.commands.parser.brigadier.string.StringParseError
import de.bixilon.minosoft.commands.parser.brigadier.string.StringParser import de.bixilon.minosoft.commands.parser.brigadier.string.StringParser
@ -95,7 +95,7 @@ internal class SuggestionChildReadingTest {
@Test @Test
fun testTrailingData() { fun testTrailingData() {
assertThrows<TrailingTextArgument> { (createCommand().getSuggestions(CommandReader("2_literal test"), CommandStack())) } assertThrows<TrailingTextError> { (createCommand().getSuggestions(CommandReader("2_literal test"), CommandStack())) }
} }
@Test @Test
@ -110,7 +110,7 @@ internal class SuggestionChildReadingTest {
@Test @Test
fun test2TrailingData() { fun test2TrailingData() {
assertThrows<TrailingTextArgument> { (createCommand().getSuggestions(CommandReader("2_literal 1_literal_2 test"), CommandStack())) } assertThrows<TrailingTextError> { (createCommand().getSuggestions(CommandReader("2_literal 1_literal_2 test"), CommandStack())) }
} }
@Test @Test
@ -135,7 +135,7 @@ internal class SuggestionChildReadingTest {
@Test @Test
fun testTrailingTextEmptyNode() { fun testTrailingTextEmptyNode() {
assertThrows<TrailingTextArgument> { RootNode().getSuggestions(CommandReader("trailing"), CommandStack()) } assertThrows<TrailingTextError> { RootNode().getSuggestions(CommandReader("trailing"), CommandStack()) }
} }
@Test @Test

View File

@ -48,4 +48,39 @@ internal class KUtilTest {
fun testInvalidSizeArrayModify() { fun testInvalidSizeArrayModify() {
assertThrows<IllegalArgumentException> { KUtil.modifyArrayIndex(0, 0) } assertThrows<IllegalArgumentException> { KUtil.modifyArrayIndex(0, 0) }
} }
@Test
fun testNonOverlappingString() {
assertEquals(KUtil.getOverlappingText("test", "invalid"), 0)
}
@Test
fun testEmptyOverlappingText() {
assertEquals(KUtil.getOverlappingText("", ""), 0)
}
@Test
fun testFullyOverlapping() {
assertEquals(KUtil.getOverlappingText("next", "next"), 4)
}
@Test
fun testSingleChatOverlapping() {
assertEquals(KUtil.getOverlappingText("next", "test"), 1)
}
@Test
fun testSingleChatOverlapping2() {
assertEquals(KUtil.getOverlappingText("n", "nix"), 1)
}
@Test
fun testSingleChatOverlapping3() {
assertEquals(KUtil.getOverlappingText("nix", "x"), 1)
}
@Test
fun testSingleChatOverlapping4() {
assertEquals(KUtil.getOverlappingText("nix", "ix"), 2)
}
} }