diff --git a/addons/gdshell/commands/autorun.gd b/addons/gdshell/commands/autorun.gd new file mode 100644 index 0000000..ba8eedf --- /dev/null +++ b/addons/gdshell/commands/autorun.gd @@ -0,0 +1,10 @@ +extends GDShellCommand + + +func _main(_argv: Array, _data) -> CommandResult: + output("Autorun here...") + #var x = await input("gimme text: ") # TODO input() blocks all following input + #output("hi %s" % x) + #execute("echo hi") +# execute("gdfetch") + return CommandResult.new() diff --git a/addons/gdshell/commands/autorun.gd.uid b/addons/gdshell/commands/autorun.gd.uid new file mode 100644 index 0000000..a25168c --- /dev/null +++ b/addons/gdshell/commands/autorun.gd.uid @@ -0,0 +1 @@ +uid://dhwdbfprk0w34 diff --git a/addons/gdshell/commands/default_commands/alias.gd b/addons/gdshell/commands/default_commands/alias.gd index 9ec8f09..5280026 100644 --- a/addons/gdshell/commands/default_commands/alias.gd +++ b/addons/gdshell/commands/default_commands/alias.gd @@ -1,32 +1,27 @@ extends GDShellCommand -func _init(): - COMMAND_AUTO_ALIASES = { - "unalias": "alias -r", - } - - -func _main(argv: Array, data) -> Dictionary: +func _main(argv: Array, data) -> CommandResult: var success: bool if not argv.size() > 2: output("Not enough arguments") - return {"error": 1, "error_string": "Not enough arguments"} + return CommandResult.new(1, "Not enough arguments") if "-r" in argv[1] or "--remove" in argv[1]: - success = _PARENT_PROCESS._PARENT_GDSHELL.command_db.remove_alias(argv[2]) - if success: - output("Alias '%s' removed" % argv[2]) - return DEFAULT_COMMAND_RESULT + _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.command_db.remove_alias(argv[2]) + return CommandResult.new() - success = _PARENT_PROCESS._PARENT_GDSHELL.command_db.add_alias(argv[1], argv[2]) - if not success: - output("Could not add alias '%s'" % argv[1]) - return {"error": 1, "error_string": "Could not add alias"} + _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.command_db.add_alias(argv[1], argv[2]) - output("Alias '%s' added" % argv[1]) - return DEFAULT_COMMAND_RESULT +# output("Alias '%s' added" % argv[1]) + return CommandResult.new() + + +func _get_command_auto_aliases() -> Dictionary: + return { + "unalias": "alias -r", + } func _get_manual() -> String: @@ -63,8 +58,8 @@ func _get_manual() -> String: -Same as [i]alias -r print[/i] """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/alias.gd.uid b/addons/gdshell/commands/default_commands/alias.gd.uid new file mode 100644 index 0000000..49cd92c --- /dev/null +++ b/addons/gdshell/commands/default_commands/alias.gd.uid @@ -0,0 +1 @@ +uid://dqkp7k311c7qq diff --git a/addons/gdshell/commands/default_commands/autorun.gd b/addons/gdshell/commands/default_commands/autorun.gd deleted file mode 100644 index c93c0fe..0000000 --- a/addons/gdshell/commands/default_commands/autorun.gd +++ /dev/null @@ -1,6 +0,0 @@ -extends GDShellCommand - - -func _main(_argv: Array, _data) -> Dictionary: -# execute("gdfetch") - return DEFAULT_COMMAND_RESULT diff --git a/addons/gdshell/commands/default_commands/bool.gd b/addons/gdshell/commands/default_commands/bool.gd index 5961da5..ebcba0c 100644 --- a/addons/gdshell/commands/default_commands/bool.gd +++ b/addons/gdshell/commands/default_commands/bool.gd @@ -1,26 +1,19 @@ extends GDShellCommand -const TRUE: Dictionary = { - "error": 0, - "data": "true", -} -const FALSE: Dictionary = { - "error": 1, - "error_string": "This is not an error, but false.", - "data": "false", -} +var TRUE: CommandResult = CommandResult.new( + 0, + "", + true +) +var FALSE: CommandResult = CommandResult.new( + 1, + "This is not an error, but false from bool command.", + false +) -func _init(): - COMMAND_AUTO_ALIASES = { - "true": "bool -t", - "false": "bool -f", - "random": "bool -r", - } - - -func _main(argv: Array, data) -> Dictionary: +func _main(argv: Array, _data) -> CommandResult: if not argv.size() > 1: return TRUE @@ -33,11 +26,19 @@ func _main(argv: Array, data) -> Dictionary: randomize() return TRUE if randi() % 2 else FALSE _: - return { - "error": ERR_INVALID_PARAMETER, - "error_string": "Parameter '%s' not recognized" % argv[1], - "data": null, - } + return CommandResult.new( + ERR_INVALID_PARAMETER, + "Parameter '%s' not recognized" % argv[1], + null + ) + + +func _get_command_auto_aliases(): + return { + "true": "bool -t", + "false": "bool -f", + "random": "bool -r", + } func _get_manual() -> String: @@ -78,8 +79,8 @@ func _get_manual() -> String: Same as: random && echo "true" || echo "false" """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/bool.gd.uid b/addons/gdshell/commands/default_commands/bool.gd.uid new file mode 100644 index 0000000..5ccbfba --- /dev/null +++ b/addons/gdshell/commands/default_commands/bool.gd.uid @@ -0,0 +1 @@ +uid://0jqep47mskax diff --git a/addons/gdshell/commands/default_commands/clear.gd b/addons/gdshell/commands/default_commands/clear.gd index 1428802..1754be9 100644 --- a/addons/gdshell/commands/default_commands/clear.gd +++ b/addons/gdshell/commands/default_commands/clear.gd @@ -1,16 +1,16 @@ extends GDShellCommand -func _init(): - COMMAND_AUTO_ALIASES = { - "cls": "clear", - } - - -func _main(_argv: Array, _data) -> Dictionary: +func _main(_argv: Array, _data) -> CommandResult: # Truly unbelieveable programming skills get_ui_handler_rich_text_label().clear() - return DEFAULT_COMMAND_RESULT + return CommandResult.new() + + +func _get_command_auto_aliases(): + return { + "cls": "clear", + } func _get_manual() -> String: @@ -33,8 +33,8 @@ func _get_manual() -> String: -Same as [i]clear[/i] """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/clear.gd.uid b/addons/gdshell/commands/default_commands/clear.gd.uid new file mode 100644 index 0000000..613e165 --- /dev/null +++ b/addons/gdshell/commands/default_commands/clear.gd.uid @@ -0,0 +1 @@ +uid://cy2ajeeqr4rvm diff --git a/addons/gdshell/commands/default_commands/echo.gd b/addons/gdshell/commands/default_commands/echo.gd index 0141e1e..ae15a26 100644 --- a/addons/gdshell/commands/default_commands/echo.gd +++ b/addons/gdshell/commands/default_commands/echo.gd @@ -1,7 +1,7 @@ extends GDShellCommand -func _main(argv: Array, data) -> Dictionary: +func _main(argv: Array, data) -> CommandResult: var out: String = "" if data != null: @@ -13,7 +13,7 @@ func _main(argv: Array, data) -> Dictionary: output(out) @warning_ignore("incompatible_ternary") - return {"data": null if out.is_empty() else out} + return CommandResult.new(OK, "", null if out.is_empty() else out) func _get_manual() -> String: @@ -40,8 +40,8 @@ SYNOPSIS -Prints Hello 1 World! """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/echo.gd.uid b/addons/gdshell/commands/default_commands/echo.gd.uid new file mode 100644 index 0000000..6f1a9e4 --- /dev/null +++ b/addons/gdshell/commands/default_commands/echo.gd.uid @@ -0,0 +1 @@ +uid://dk2m8fw8cijpa diff --git a/addons/gdshell/commands/default_commands/gdfetch.gd b/addons/gdshell/commands/default_commands/gdfetch.gd index f95042e..6022c4e 100644 --- a/addons/gdshell/commands/default_commands/gdfetch.gd +++ b/addons/gdshell/commands/default_commands/gdfetch.gd @@ -35,13 +35,7 @@ const LOGO: String = ( ) -func _init(): - COMMAND_AUTO_ALIASES = { - "neofetch": "gdfetch --i-am-a-linux-nerd-and-tried-to-use-neofetch", - } - - -func _main(argv: Array, data) -> Dictionary: +func _main(argv: Array, data) -> CommandResult: var info: Dictionary = get_info() if "--i-am-a-linux-nerd-and-tried-to-use-neofetch" in argv: @@ -50,7 +44,7 @@ func _main(argv: Array, data) -> Dictionary: if not ("-s" in argv or "--silent" in argv): output(construct_output(LOGO, info), false) - return {"data": info} + return CommandResult.new(0, "", info) func construct_output(graphics: String, info: Dictionary, skip_lines: int = 3) -> String: @@ -85,6 +79,12 @@ static func get_info() -> Dictionary: } +func _get_command_auto_aliases(): + return { + "neofetch": "gdfetch --i-am-a-linux-nerd-and-tried-to-use-neofetch", + } + + func _get_manual() -> String: return ( """ @@ -116,8 +116,8 @@ func _get_manual() -> String: Can be used as a input for other commands when called silently. """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/gdfetch.gd.uid b/addons/gdshell/commands/default_commands/gdfetch.gd.uid new file mode 100644 index 0000000..a4c3ec7 --- /dev/null +++ b/addons/gdshell/commands/default_commands/gdfetch.gd.uid @@ -0,0 +1 @@ +uid://dpionqq57kuh3 diff --git a/addons/gdshell/commands/default_commands/man.gd b/addons/gdshell/commands/default_commands/man.gd index cd3c556..c0b6fba 100644 --- a/addons/gdshell/commands/default_commands/man.gd +++ b/addons/gdshell/commands/default_commands/man.gd @@ -5,27 +5,20 @@ const LIST_FLAGS: Array[String] = ["l", "L", "list", "LIST"] const SILENT_FLAGS: Array[String] = ["s", "S", "silent", "SILENT"] -func _init(): - COMMAND_AUTO_ALIASES = { - "manual": "man", - "help": "man", - } - - -func _main(argv: Array, _data) -> Dictionary: +func _main(argv: Array, _data) -> CommandResult: if not argv.size() > 1: output("What manual page do you want? For example, try '[b]man man[/b]'\nTo see the list of all commands run '[b]man --list[/b]'") - return DEFAULT_COMMAND_RESULT + return CommandResult.new() var options: Dictionary = GDShellCommand.argv_parse_options(argv, true, false) if LIST_FLAGS.any(func(option): return option in options): # If any LIST_FLAG is in options output("[b][color=AQUAMARINE]Available GDShell commands:[/color][/b]") - for command_name in _PARENT_PROCESS._PARENT_GDSHELL.command_db.get_all_command_names(): + for command_name in _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.command_db.get_all_command_names(): output("[color=BISQUE]%s[/color]" % command_name) if not argv.size() > options.keys().size() + 1: - return DEFAULT_COMMAND_RESULT + return CommandResult.new() var manual: String = "" for i in range(1, argv.size()): # first non-option arg @@ -34,17 +27,17 @@ func _main(argv: Array, _data) -> Dictionary: break if manual.is_empty(): - return {"error": 1, "error_string": "No manual", "data": null} + return CommandResult.new(1, "No manual", null) if not SILENT_FLAGS.any(func(option): return option in options): # If NOT any LIST_FLAG is in options var line: int = get_ui_handler_rich_text_label().get_line_count() output(manual) get_ui_handler_rich_text_label().call_deferred(&"scroll_to_line", line) - return {"data": manual} + return CommandResult.new(0, "", manual) func get_command_manual(command_name: String) -> String: - var command_db: GDShellCommandDB = _PARENT_PROCESS._PARENT_GDSHELL.command_db + var command_db: GDShellCommandDB = _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.command_db # unalias the name while true: if not command_name in command_db._aliases: @@ -67,6 +60,13 @@ func get_command_manual(command_name: String) -> String: return manual +func _get_command_auto_aliases() -> Dictionary: + return { + "manual": "man", + "help": "man", + } + + func _get_manual() -> String: return ( """ @@ -90,8 +90,8 @@ func _get_manual() -> String: -Prints the manual for the [i]man[/i] command """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/man.gd.uid b/addons/gdshell/commands/default_commands/man.gd.uid new file mode 100644 index 0000000..14b5cdd --- /dev/null +++ b/addons/gdshell/commands/default_commands/man.gd.uid @@ -0,0 +1 @@ +uid://dwjw45cqys264 diff --git a/addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd b/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd similarity index 87% rename from addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd rename to addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd index 423dd69..e135496 100644 --- a/addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd +++ b/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd @@ -6,6 +6,7 @@ const MONITOR_NODE_NAME: String = "GDShellMonitorOverlayIntegration" const OPTIONS_FLAGS: Array[String] = ["o", "O", "options", "OPTIONS"] +# TODO implement my own PR # Workaround until https://github.com/godotengine/godot/pull/69624 gets merged const TYPE_NAMES: Array[String] = [ "Nil", @@ -49,23 +50,23 @@ const TYPE_NAMES: Array[String] = [ ] -func _main(argv: Array, _data) -> Dictionary: +func _main(argv: Array, _data) -> CommandResult: var monitor: Node = _get_monitor_overlay() if monitor == null: output("Cannot access Monitor Overlay. Make sure you have 'Monitor Overlay' plugin installed and try again") - return { - "error": 1, - "error_string": "Cannot access Monitor Overlay. Make sure you have 'Monitor Overlay' plugin installed and try again", - "data": null, - } + return CommandResult.new( + 1, + "Cannot access Monitor Overlay. Make sure you have 'Monitor Overlay' plugin installed and try again", + null + ) if argv.size() == 1: output("Not enought arguments. Run 'man monitor' to see all available options") - return { - "error": 2, - "error_string": "Not enought arguments. Run 'man monitor' to see all available options", - "data": null, - } + return CommandResult.new( + 2, + "Not enought arguments. Run 'man monitor' to see all available options", + null + ) var safe_to_edit_properties: Array[Dictionary] = _get_monitor_overlay_safe_to_edit_properties(monitor) var options: Dictionary = GDShellCommand.argv_parse_options(argv, true, false) @@ -79,11 +80,11 @@ func _main(argv: Array, _data) -> Dictionary: _edit_monitor_properties_with_options(monitor, safe_to_edit_properties, options) - return DEFAULT_COMMAND_RESULT + return CommandResult.new() func _get_monitor_overlay() -> Node: - if not _PARENT_PROCESS._PARENT_GDSHELL.has_node(NodePath(MONITOR_NODE_NAME)): + if not _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.has_node(NodePath(MONITOR_NODE_NAME)): if not ResourceLoader.exists(MONITOR_FILE_PATH): return null # MonitorOverlay is not installed @@ -94,9 +95,9 @@ func _get_monitor_overlay() -> Node: monitor.unique_name_in_owner = true # disable the fps monitor as it is enabled by default monitor.set("fps", false) - _PARENT_PROCESS._PARENT_GDSHELL.add_child(monitor) + _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.add_child(monitor) - return _PARENT_PROCESS._PARENT_GDSHELL.get_node(NodePath(MONITOR_NODE_NAME)) + return _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.get_node(NodePath(MONITOR_NODE_NAME)) # returns a list of properties that are used for MonitorOverlay UI control @@ -165,6 +166,6 @@ func _get_manual() -> String: [i]monitor -fps=true --process=true --physics_process=false --sampling_rate=10[/i] -Enables fps and process monitors, disables physics_process monitor and sets sampling rate to 10 """.format({ - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), }) diff --git a/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd.uid b/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd.uid new file mode 100644 index 0000000..ae88a01 --- /dev/null +++ b/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd.uid @@ -0,0 +1 @@ +uid://pw1ksoelnhof diff --git a/addons/gdshell/demo/demo.gd b/addons/gdshell/demo/demo.gd index a84c259..e926a71 100644 --- a/addons/gdshell/demo/demo.gd +++ b/addons/gdshell/demo/demo.gd @@ -3,21 +3,28 @@ extends CanvasItem const ICON_TO_VIEWPORT_RATIO: float = 0.309018 -var icon: Sprite2D +@onready var icon: Sprite2D = %GDShellIcon +@onready var label: Label = %Label func _ready() -> void: - $GDShellIcon/Label.text = "Press '%s' to toggle GDShell" % InputMap.action_get_events(GDShell.UI_TOGGLE_ACTION)[0].as_text_keycode() icon = $GDShellIcon + @warning_ignore("return_value_discarded") get_viewport().size_changed.connect(update_icon) update_icon() + + var gdshell_ui_toggle_action_input_events: Array[InputEvent] = InputMap.action_get_events(GDShell.ui_handler._UI_TOGGLE_ACTION) + if gdshell_ui_toggle_action_input_events.is_empty(): + label.text = "No InputEvent is set for GDShell Ui Toggle Action.\nSet an action in settings at 'gdshell/settings/ui/ui_toggle_action'." + else: + label.text = "Press '%s' to toggle GDShell" % (gdshell_ui_toggle_action_input_events[0] as InputEventKey).as_text_keycode() # responsive icon func update_icon() -> void: # scale the icon so that it takes up ICON_TO_VIEWPORT_RATIO of the viewport - var min_viewport_side: int = min(get_viewport_rect().size.x, get_viewport_rect().size.y) - var max_texture_side: int = max(icon.texture.get_size().x, icon.texture.get_size().y) + var min_viewport_side: float = min(get_viewport_rect().size.x, get_viewport_rect().size.y) + var max_texture_side: float = max(icon.texture.get_size().x, icon.texture.get_size().y) var scale_factor: float = (min_viewport_side / max_texture_side) * ICON_TO_VIEWPORT_RATIO icon.scale = Vector2(scale_factor, scale_factor) diff --git a/addons/gdshell/demo/demo.gd.uid b/addons/gdshell/demo/demo.gd.uid new file mode 100644 index 0000000..0579cdf --- /dev/null +++ b/addons/gdshell/demo/demo.gd.uid @@ -0,0 +1 @@ +uid://bg5laxgioyd86 diff --git a/addons/gdshell/demo/demo.tscn b/addons/gdshell/demo/demo.tscn index 5e01afc..53208cc 100644 --- a/addons/gdshell/demo/demo.tscn +++ b/addons/gdshell/demo/demo.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://cil8b81ctftx3"] -[ext_resource type="Script" path="res://addons/gdshell/demo/demo.gd" id="1_cwk1x"] +[ext_resource type="Script" uid="uid://bg5laxgioyd86" path="res://addons/gdshell/demo/demo.gd" id="1_cwk1x"] [ext_resource type="Texture2D" uid="uid://bmmfysofbp50p" path="res://addons/gdshell/icon.png" id="2_hso4g"] [node name="GDShellDemo" type="Control"] @@ -13,10 +13,12 @@ grow_vertical = 2 script = ExtResource("1_cwk1x") [node name="GDShellIcon" type="Sprite2D" parent="."] +unique_name_in_owner = true texture = ExtResource("2_hso4g") [node name="Label" type="Label" parent="GDShellIcon"] -anchors_preset = -1 +unique_name_in_owner = true +anchors_preset = 8 anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 @@ -27,3 +29,4 @@ offset_right = -13.0 offset_bottom = 32.0 grow_horizontal = 2 text = "SAMPLE TEXT" +horizontal_alignment = 1 diff --git a/addons/gdshell/gdshell_editor_plugin.gd.uid b/addons/gdshell/gdshell_editor_plugin.gd.uid new file mode 100644 index 0000000..e48fdb7 --- /dev/null +++ b/addons/gdshell/gdshell_editor_plugin.gd.uid @@ -0,0 +1 @@ +uid://cdl3y2evxe7ir diff --git a/addons/gdshell/plugin.cfg b/addons/gdshell/plugin.cfg index 7c49478..353e40b 100644 --- a/addons/gdshell/plugin.cfg +++ b/addons/gdshell/plugin.cfg @@ -3,5 +3,5 @@ name="GDShell" description="Feature-packed customizable in-game console for development, debugging, cheats, etc... for Godot 4" author="Jakub Janšta (Kubulambula)" -version="1.0-dev2" +version="1.0-dev3" script="gdshell_editor_plugin.gd" diff --git a/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd new file mode 100644 index 0000000..ca4b5e0 --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd @@ -0,0 +1,174 @@ +@icon("res://addons/gdshell/icon.png") +class_name GDShellExpressionCompiler +extends RefCounted + + +class CompilerResult extends RefCounted: + enum Status { + OK, + ERROR, + UNTERMINATED, + } + + var result: Dictionary + var status: Status + var input_expression: String + var description: String + var input_expression_error_start_index: int + var input_expression_error_length: int + + func _init(_result: Dictionary, _status: Status, _input_expression: String, _description: String, _input_expression_error_start_index: int, _input_expression_error_length: int) -> void: + result = _result + status = _status + input_expression = _input_expression + description = _description + input_expression_error_start_index = _input_expression_error_start_index + input_expression_error_length = _input_expression_error_length + + func _to_string() -> String: + return "{CompilerResult: %s, description: \"%s\"}" % [Status.find_key(status), description] + + +static func compile(input_expression: String) -> CompilerResult: + # Tokenize the input + var tokenizer_result: GDShellExpressionTokenizer.TokenizerResult = GDShellExpressionTokenizer.tokenize(input_expression) + + if tokenizer_result.status == GDShellExpressionTokenizer.TokenizerResult.Status.ERROR: + return CompilerResult.new( + {}, + CompilerResult.Status.ERROR, + input_expression, + tokenizer_result.description, + tokenizer_result.result[-1].start_char_index, + tokenizer_result.result[-1].consumed_chars + ) + if tokenizer_result.status == GDShellExpressionTokenizer.TokenizerResult.Status.UNTERMINATED: + return CompilerResult.new( + {}, + CompilerResult.Status.UNTERMINATED, + input_expression, + "unterminated expression", + tokenizer_result.result[-1].start_char_index, + tokenizer_result.result[-1].consumed_chars + ) + # empty input - dont even bother with parsing + if tokenizer_result.result.is_empty(): + return CompilerResult.new( + {}, + CompilerResult.Status.OK, + input_expression, + tokenizer_result.description, + 0, + 0 + ) + + # Parse the tokenized input + var parser_result: GDShellExpressionParser.ParserResult = GDShellExpressionParser.parse(tokenizer_result.result) + return CompilerResult.new( + parser_result.result, + CompilerResult.Status.OK if parser_result.status == GDShellExpressionParser.ParserResult.Status.OK else CompilerResult.Status.ERROR, + input_expression, + parser_result.description, + parser_result.input_expression_error_start_index, + parser_result.input_expression_error_length + ) + + +static func is_command_expression_valid(command_expression: Dictionary, error_info: bool = false, command_db: GDShellCommandDB = null) -> bool: + if not command_expression.has_all(["type", "name", "args"]): + return false + # type + if command_expression["type"] != "command": + return false + # name + if typeof(command_expression["name"]) != TYPE_STRING: + return false + if command_db != null: + push_error("CommandDB validation not yet implemented.") + # args + if typeof(command_expression["args"]) != TYPE_ARRAY: + return false + @warning_ignore("unsafe_method_access") + if command_expression["args"].any( + func(arg: Variant) -> bool: + return typeof(arg) != TYPE_STRING + ): + return false + # for reporting where the error occurred + if error_info: + if not command_expression.has_all(["index", "lenght"]): + return false + # index + if typeof(command_expression["index"]) != TYPE_INT: + return false + if command_expression["index"] < 0: + return false + # length + if typeof(command_expression["length"]) != TYPE_INT: + return false + if command_expression["length"] < 1: + return false + + return true + + +static func is_operator_expression_valid(operator_expression: Dictionary, error_info: bool = false) -> bool: + if not operator_expression.has_all(["type", "operator"]): + return false + # type + if operator_expression["type"] != "operator": + return false + # for reporting where the error occurred + if error_info: + if not operator_expression.has_all(["index", "lenght"]): + return false + # index + if typeof(operator_expression["index"]) != TYPE_INT: + return false + if operator_expression["index"] < 0: + return false + # length + if typeof(operator_expression["length"]) != TYPE_INT: + return false + if operator_expression["length"] < 1: + return false + # operator + if typeof(operator_expression["operator"]) != TYPE_STRING: + return false + match operator_expression["operator"]: + "!": + if not operator_expression.has("right"): + return false + if typeof(operator_expression["right"]) != TYPE_DICTIONARY: + return false + "&": + if not operator_expression.has("left"): + return false + if typeof(operator_expression["left"]) != TYPE_DICTIONARY: + return false + "|", "||", "&&", ";": + if not operator_expression.has_all(["left", "right"]): + return false + if typeof(operator_expression["left"]) != TYPE_DICTIONARY: + return false + if typeof(operator_expression["right"]) != TYPE_DICTIONARY: + return false + + return true + + +static func is_expression_valid(expression: Dictionary, error_info: bool = false, command_db: GDShellCommandDB = null) -> bool: + if expression.get("type") == "command": + return is_command_expression_valid(expression, error_info, command_db) + + elif expression.get("type") == "operator": + if is_operator_expression_valid(expression, error_info) == false: + return false + if expression.has("left"): + if is_expression_valid(expression["left"], error_info, command_db) == false: + return false + if expression.has("right"): + if is_expression_valid(expression["right"], error_info, command_db) == false: + return false + + return true diff --git a/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd.uid b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd.uid new file mode 100644 index 0000000..e353dc6 --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd.uid @@ -0,0 +1 @@ +uid://d0a8e4sbpnpa3 diff --git a/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd b/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd new file mode 100644 index 0000000..729db27 --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd @@ -0,0 +1,272 @@ +@icon("res://addons/gdshell/icon.png") +class_name GDShellExpressionTokenizer +extends RefCounted + + +class Token extends RefCounted: + enum Type { + # Error token - content of the token is an error message + ERROR, + # Parser tokens + EXPRESSION, + EXPRESSION_END, + EXPRESSION_HANDLE, + # Helper tokenizer token + SPACE, + # Tokenizer tokens + WORD, + WORD_UNTERMINATED, +# OPERATOR_EXPAND, + OPERATOR_NOT, + OPERATOR_BACKGROUND, + OPERATOR_AND, + OPERATOR_PIPE, + OPERATOR_OR, + OPERATOR_SEQUENCE, + OPERATOR_OPENING_PARENTHESIS, + OPERATOR_CLOSING_PARENTHESIS, + } + + var type: Type + var content: String + var start_char_index: int + var consumed_chars: int + + func _init(_type: Type, _content: String, _start_char_index: int, _consumed_chars: int) -> void: + self.type = _type + self.content = _content + self.start_char_index = _start_char_index + self.consumed_chars = _consumed_chars + + func _to_string() -> String: + return "{Token: %s, Content: \"%s\", Start char index: %s}" % [str(Type.find_key(type)), content, start_char_index] + + +class TokenizerResult extends RefCounted: + enum Status { + OK, + ERROR, + UNTERMINATED, + } + + var result: Array[Token] + var status: Status + var description: String + + func _init(_result: Array[Token], _status: TokenizerResult.Status, _description: String) -> void: + result = _result + status = _status + description = _description + + +static func tokenize(input_expression: String) -> TokenizerResult: + var tokens: Array[Token] = [] + var current_token: Token = null + var current_char_index: int = 0 + + if input_expression.is_empty(): + return TokenizerResult.new([], TokenizerResult.Status.OK, "empty input expression") + + while current_char_index < input_expression.length(): + match input_expression[current_char_index]: + " ": + current_token = _tokenize_space(input_expression, current_char_index) + ";": + current_token = _tokenize_semicolon(input_expression, current_char_index) + "!": + current_token = _tokenize_exclamation(input_expression, current_char_index) + #"$": # TODO variable operator + #tokens.push_back(_tokenize_dollar_sign(input_expression, current_char)) + "&": + current_token = _tokenize_and(input_expression, current_char_index) + "|": + current_token = _tokenize_vertical_slash(input_expression, current_char_index) + "(", ")": + current_token = _tokenize_parenthesis(input_expression, current_char_index) + "\"", "\'": + current_token = _tokenize_quote(input_expression, current_char_index) + _: + current_token = _tokenize_text(input_expression, current_char_index) + + current_char_index += current_token.consumed_chars + tokens.push_back(current_token) + + if current_token.type == Token.Type.ERROR: + return TokenizerResult.new( + tokens, + TokenizerResult.Status.ERROR, + current_token.content + ) + if current_token.type == Token.Type.WORD_UNTERMINATED: + return TokenizerResult.new( + tokens, + TokenizerResult.Status.UNTERMINATED, + "unterminated expression" + ) + + return TokenizerResult.new( + _filter_out_space_tokens(_merge_word_tokens(tokens)), + TokenizerResult.Status.OK, + "OK" + ) + + +static func _tokenize_space(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != " ": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as space" % input_expression[start_char_index], + start_char_index, + 0 + ) + + var space_chars_consumed: int = 0 + while start_char_index + space_chars_consumed < input_expression.length() and input_expression[start_char_index + space_chars_consumed] == " ": + space_chars_consumed += 1 + return Token.new(Token.Type.SPACE, " ", start_char_index, space_chars_consumed) + + +static func _tokenize_semicolon(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != ";": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as semicolon" % input_expression[start_char_index], + start_char_index, + 0 + ) + + return Token.new(Token.Type.OPERATOR_SEQUENCE, ";", start_char_index, 1) + + +static func _tokenize_exclamation(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != "!": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as exclamation" % input_expression[start_char_index], + start_char_index, + 0 + ) + + return Token.new(Token.Type.OPERATOR_NOT, "!", start_char_index, 1) + + +static func _tokenize_and(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != "&": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as and" % input_expression[start_char_index], + start_char_index, + 0 + ) + + if start_char_index < input_expression.length() - 1 and input_expression[start_char_index + 1] == "&": + return Token.new(Token.Type.OPERATOR_AND, "&&", start_char_index, 2) + return Token.new(Token.Type.OPERATOR_BACKGROUND, "&", start_char_index, 1) + + +static func _tokenize_vertical_slash(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != "|": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as vertical slash" % input_expression[start_char_index], + start_char_index, + 0 + ) + + if start_char_index < input_expression.length() - 1 and input_expression[start_char_index + 1] == "|": + return Token.new(Token.Type.OPERATOR_OR, "||", start_char_index, 2) + return Token.new(Token.Type.OPERATOR_PIPE, "|", start_char_index, 1) + + +static func _tokenize_parenthesis(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] == "(": + return Token.new(Token.Type.OPERATOR_OPENING_PARENTHESIS, "(", start_char_index, 1) + elif input_expression[start_char_index] == ")": + return Token.new(Token.Type.OPERATOR_CLOSING_PARENTHESIS, ")", start_char_index, 1) + else: + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as parenthesis" % input_expression[start_char_index], + start_char_index, + 0 + ) + + +static func _tokenize_quote(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != "\"" and input_expression[start_char_index] != "'": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as quote" % input_expression[start_char_index], + start_char_index, + 0 + ) + + var content: String = "" + for i: int in range(start_char_index + 1, input_expression.length()): # Skip the opening quote and start on the char right after + if input_expression[i] == input_expression[start_char_index] and input_expression[i - 1] != "\\": # check for string end + return Token.new( + Token.Type.WORD, + content.c_unescape(), + start_char_index, + content.length() + 2 # accounts for the starting and ending quotes + ) + content += input_expression[i] + + # End of input_expression was reached without finding a closing quote + return Token.new( + Token.Type.WORD_UNTERMINATED, + content.c_unescape(), + start_char_index, + content.length() + 1 # accounts just for the starting quote + ) + + +static func _tokenize_text(input_expression: String, start_char_index: int) -> Token: + var content: String = "" + + for i: int in range(start_char_index, input_expression.length()): + # check if the character should end the WORD token. + if input_expression[i] in [" ", ";", "&", "|", "!", "(", ")", "\'", "\"", "\\", "\a", "\b", "\f", "\n", "\r", "\t", "\v"]: + break + content += input_expression[i] + + if content.is_empty(): + return Token.new( + Token.Type.ERROR, + "cannot start tokenizing word", + start_char_index, + 0 + ) + + return Token.new( + Token.Type.WORD, + content, + start_char_index, + content.length() + ) + + +## Merges WORD tokens if they are not separated by any other token +static func _merge_word_tokens(tokens: Array[Token]) -> Array[Token]: + if tokens.is_empty(): + return tokens + # We now know that tokens is not empty so we append the first token for later simplification + var merged_tokens: Array[Token] = [tokens[0]] + + # Start from the second token as we already appended the first + for i: int in range(1, tokens.size()): + if tokens[i].type == Token.Type.WORD and merged_tokens[-1].type == Token.Type.WORD: + merged_tokens[-1].content += tokens[i].content + merged_tokens[-1].consumed_chars += tokens[i].consumed_chars + else: + merged_tokens.append(tokens[i]) + + return merged_tokens + + +## Filters out SPACE tokens as after _merge_word_tokens() they are useless and it simplifies next operations. +static func _filter_out_space_tokens(tokens: Array[Token]) -> Array[Token]: + return tokens.filter( + func is_token_not_space(token: Token) -> bool: + return token.type != Token.Type.SPACE + ) diff --git a/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd.uid b/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd.uid new file mode 100644 index 0000000..e5378a2 --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd.uid @@ -0,0 +1 @@ +uid://drg1k066fmiyl diff --git a/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd new file mode 100644 index 0000000..916df02 --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd @@ -0,0 +1,402 @@ +@icon("res://addons/gdshell/icon.png") +class_name GDShellExpressionParser +extends RefCounted + + +## Two dimensional Array for precedence parser action lookup. +## [br] - < Shift +## [br] - > Reduce +## [br] - = Push +## [br] - ! Error +## [br] - ? Unknown token (not present in this table, but used internally for errors) +## [br] - . End +const _PRECEDENCE_TABLE: Array[Array] = [ + # ! & && | || ; WORD ( ) $ + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &"!", &">"], # ! + [&">", &"!", &">", &">", &">", &">", &"!", &"!", &">", &">"], # & + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &">", &">"], # && + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &">", &">"], # | + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &">", &">"], # || + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &">", &">"], # ; + [&"!", &">", &">", &">", &">", &">", &"=", &"!", &">", &">"], # WORD + [&"<", &"<", &"<", &"<", &"<", &"<", &"<", &"<", &"=", &"!"], # ( + [&"<", &">", &">", &">", &">", &">", &"!", &"!", &">", &">"], # ) + [&"<", &"<", &"<", &"<", &"<", &"<", &"<", &"<", &"!", &"."], # $ +] + +## The index of the Token in this array is the index to _PRECEDENCE_TABLE array. +const _PRECEDENCE_TABLE_KEYS: Array[GDShellExpressionTokenizer.Token.Type] = [ + GDShellExpressionTokenizer.Token.Type.OPERATOR_NOT, + GDShellExpressionTokenizer.Token.Type.OPERATOR_BACKGROUND, + GDShellExpressionTokenizer.Token.Type.OPERATOR_AND, + GDShellExpressionTokenizer.Token.Type.OPERATOR_PIPE, + GDShellExpressionTokenizer.Token.Type.OPERATOR_OR, + GDShellExpressionTokenizer.Token.Type.OPERATOR_SEQUENCE, + GDShellExpressionTokenizer.Token.Type.WORD, + GDShellExpressionTokenizer.Token.Type.OPERATOR_OPENING_PARENTHESIS, + GDShellExpressionTokenizer.Token.Type.OPERATOR_CLOSING_PARENTHESIS, + GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, +] + + +class ParserResult extends RefCounted: + enum Status { + OK, + ERROR, + } + + var result: Dictionary + var status: Status + var description: String + var input_expression_error_start_index: int + var input_expression_error_length: int + + func _init(_result: Dictionary, _status: Status, _description: String, _input_expression_error_start_index: int, _input_expression_error_length: int) -> void: + result = _result + status = _status + description = _description + input_expression_error_start_index = _input_expression_error_start_index + input_expression_error_length = _input_expression_error_length + + +static func parse(tokens: Array[GDShellExpressionTokenizer.Token]) -> ParserResult: + # Append EXPRESSION_END Token to the input tokens to make precedence easier + tokens.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, "$", 0, 0)) + var current_token_index: int = 0 # Index into tokens array + # Helper Token stack for precedence - initialize it with EXPRESSION_END Token + var token_stack: Array[GDShellExpressionTokenizer.Token] = [GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, "$", 0, 0)] + # Expresion stack for expression tree building + var expression_node_stack: Array[Dictionary] = [] + + while current_token_index < tokens.size(): + var topmost_terminal_index: int = _parse_precedence_get_topmost_terminal_index(token_stack) + if topmost_terminal_index == -1: + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "no terminal found in token stack", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + match _parse_get_precedence_action(token_stack[topmost_terminal_index], tokens[current_token_index]): + &"<": # Shift + # Insert handle for expression reduction + if token_stack.insert(topmost_terminal_index + 1, GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, "<", 0, 0)) != OK: + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "cannot insert EXPRESSION_HANDLE", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + token_stack.push_back(tokens[current_token_index]) + current_token_index += 1 + + &">": # Reduce + if _parse_precedence_reduce_to_expression(token_stack, expression_node_stack) == false: + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "cannot reduce stack to EXPRESSION", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + &"=": # Push + token_stack.push_back(tokens[current_token_index]) + current_token_index += 1 + + &"!": # Error + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "unexpected token", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + &"?": # Unexpected token while parsing + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "unknown, unparseable or unimplemented Token", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + &".": # OK + return ParserResult.new( + {} if expression_node_stack.is_empty() else expression_node_stack[0], + ParserResult.Status.OK, + "OK", + 0, + 0 + ) + + var unknown_action: # Unknown action + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "unknown precedence parser action '%s'" % str(unknown_action), + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "parsing ended prematurely due to token buffer out of bounds", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + +static func _parse_get_reduceable_tokens(token_stack: Array[GDShellExpressionTokenizer.Token]) -> Array[GDShellExpressionTokenizer.Token]: + # Traverse token_stack array backwards + # From the back get all the tokens until expression handle is encountered + for i: int in range(token_stack.size() - 1, -1, -1): + if token_stack[i].type == GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE: + return token_stack.slice(i) + return [] # No expression handle found - nothing can be reduced + + +static func _parse_precedence_is_token_array_type_patern_match(tokens: Array[GDShellExpressionTokenizer.Token], pattern: Array[GDShellExpressionTokenizer.Token.Type]) -> bool: + if tokens.size() != pattern.size(): + return false + for i: int in tokens.size(): + if tokens[i].type != pattern[i]: + return false + return true + + +static func _parse_precedence_reduce_to_expression(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: + var reduceable_tokens: Array[GDShellExpressionTokenizer.Token] = _parse_get_reduceable_tokens(token_stack) + if reduceable_tokens.is_empty(): + push_error("[GDShell] No reduceable tokens found. Missing EXPRESSION_HANDLE Token?") + return false + + var reduceable_tokens_types: Array = reduceable_tokens.map( + func(token: GDShellExpressionTokenizer.Token) -> GDShellExpressionTokenizer.Token.Type: + return token.type + ) + + match reduceable_tokens_types: + # E -> WORD+ + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.WORD, ..]: + # Check if all Tokens after EXPRESSION_HANDLE are WORDs because of the open ended pattern + if reduceable_tokens_types.slice(1).all( + func(token_type: GDShellExpressionTokenizer.Token.Type) -> bool: + return token_type == GDShellExpressionTokenizer.Token.Type.WORD + ): + return _parse_reduce_words(token_stack, expression_node_stack) + # E -> (E) + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.OPERATOR_OPENING_PARENTHESIS, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_CLOSING_PARENTHESIS]: + return _parse_reduce_parenthesis(token_stack) + # E -> !E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.OPERATOR_NOT, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + return _parse_reduce_not(token_stack, expression_node_stack) + # E -> E& + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_BACKGROUND]: + return _parse_reduce_background(token_stack, expression_node_stack) + # E -> E && E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_AND, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + return _parse_reduce_and(token_stack, expression_node_stack) + # E -> E | E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_PIPE, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + return _parse_reduce_pipe(token_stack, expression_node_stack) + # E -> E || E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_OR, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + return _parse_reduce_or(token_stack, expression_node_stack) + # E -> E ; E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_SEQUENCE, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + return _parse_reduce_sequence(token_stack, expression_node_stack) + # ! -> () + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.OPERATOR_OPENING_PARENTHESIS, GDShellExpressionTokenizer.Token.Type.OPERATOR_CLOSING_PARENTHESIS]: + # Tried to reduce empty parantheses. This is an error in the input command string. + return false + + # ! -> .. + push_error("[GDShell] Cannot reduce Tokens. No matching reduction rule found for pattern: %s" % str( + reduceable_tokens_types.map( + func(token_type: GDShellExpressionTokenizer.Token.Type) -> String: + return str(GDShellExpressionTokenizer.Token.Type.find_key(token_type))) + ) + ) + return false + + +## E -> WORD+ +static func _parse_reduce_words(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: + var word_tokens: Array[GDShellExpressionTokenizer.Token] = [] + while token_stack.back() != null: + var current_token: GDShellExpressionTokenizer.Token = token_stack.pop_back() + if current_token.type != GDShellExpressionTokenizer.Token.Type.WORD: + break + word_tokens.push_front(current_token) + + if word_tokens.is_empty(): + push_error("[GDShell] No reduceable WORDs.") + return false + + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + expression_node_stack.push_back({ + "type": "command", + "index": word_tokens[0].start_char_index, + "length": word_tokens[0].consumed_chars, + "name": word_tokens.pop_front().content, + "args": word_tokens.map( + func(word_token: GDShellExpressionTokenizer.Token) -> String: + return word_token.content + ), + }) + return true + + +## E -> (E) +static func _parse_reduce_parenthesis(token_stack: Array[GDShellExpressionTokenizer.Token]) -> bool: + token_stack.pop_back() # ) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # ( + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true + + +## E -> !E +static func _parse_reduce_not(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: + var right: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": token_stack[-2].consumed_chars, + "operator": "!", + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_NOT + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true + + +## E -> E& +static func _parse_reduce_background(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-1].start_char_index, + "length": token_stack[-2].consumed_chars, + "operator": "&", + "left": left, + }) + token_stack.pop_back() # OPERATOR_BACKGROUND + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true + + +## E -> E && E +static func _parse_reduce_and(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: + var right: Dictionary = expression_node_stack.pop_back() + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": token_stack[-2].consumed_chars, + "operator": "&&", + "left": left, + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_AND + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true + + +## E -> E | E +static func _parse_reduce_pipe(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: + var right: Dictionary = expression_node_stack.pop_back() + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": token_stack[-2].consumed_chars, + "operator": "|", + "left": left, + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_PIPE + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true + + +## E -> E || E +static func _parse_reduce_or(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: + var right: Dictionary = expression_node_stack.pop_back() + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": token_stack[-2].consumed_chars, + "operator": "||", + "left": left, + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_OR + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true + + +## E -> E ; E +static func _parse_reduce_sequence(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: + var right: Dictionary = expression_node_stack.pop_back() + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": token_stack[-2].consumed_chars, + "operator": ";", + "left": left, + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_SEQUENCE + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true + + +## Finds the topmost terminal (not Expression) on the [param token_stack]. +static func _parse_precedence_get_topmost_terminal_index(token_stack: Array[GDShellExpressionTokenizer.Token]) -> int: + # Traverse token_stack array backwards + for i: int in range(token_stack.size() - 1, -1, -1): + if token_stack[i].type == GDShellExpressionTokenizer.Token.Type.EXPRESSION: + continue + return i + push_error("[GDShell] No terminal present in the Token stack. Missing EXPRESSION_END Token?") + return -1 + + +## Finds the tokens index in the [member _PRECEDENCE_TABLE_KEYS]. +static func _parse_get_precedence_index(token: GDShellExpressionTokenizer.Token) -> int: + return _PRECEDENCE_TABLE_KEYS.find(token.type) + + +## Given two [GDShellExpressionTokenizer.Token]s, returns the required action for the precedence parser. +static func _parse_get_precedence_action(stack_token: GDShellExpressionTokenizer.Token, current_token: GDShellExpressionTokenizer.Token) -> String: + var stack_token_precedence_index: int = _parse_get_precedence_index(stack_token) + var current_token_precedence_index: int = _parse_get_precedence_index(current_token) + if stack_token_precedence_index == -1 or current_token_precedence_index == -1: + return &"?" # Unparseable unknown token - might be unimplemented + return _PRECEDENCE_TABLE[stack_token_precedence_index][current_token_precedence_index] diff --git a/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd.uid b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd.uid new file mode 100644 index 0000000..bc4880e --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd.uid @@ -0,0 +1 @@ +uid://dpyjxkrr2illb diff --git a/addons/gdshell/scripts/gdshell_command.gd b/addons/gdshell/scripts/gdshell_command.gd index 646cb82..2860e41 100644 --- a/addons/gdshell/scripts/gdshell_command.gd +++ b/addons/gdshell/scripts/gdshell_command.gd @@ -3,43 +3,54 @@ class_name GDShellCommand extends Node -signal command_end +class CommandResult: + var err: int + var err_string: String + var data: Variant + + func _init(_err: int=OK, _err_string: String="", _data: Variant=null) -> void: + err = _err + err_string = "No error description." if _err != OK and _err_string.is_empty() else _err_string + data = _data -const DEFAULT_COMMAND_RESULT: Dictionary = { - "error": 0, - "error_string": "No error description", - "data": null, -} -@warning_ignore("unsafe_method_access") -var COMMAND_NAME: String = get_script().get_path().get_file().get_basename() -var COMMAND_AUTO_ALIASES: Dictionary = {} +signal command_end + -var _PARENT_PROCESS: GDShellCommandRunner +var _PARENT_COMMAND_RUNNER: GDShellCommandRunner -func _main(_argv: Array, _data) -> Dictionary: - return DEFAULT_COMMAND_RESULT +func _main(_argv: Array[String], _data: CommandResult) -> CommandResult: + return CommandResult.new() -func execute(command: String) -> Dictionary: - return await _PARENT_PROCESS._handle_execute(command) +func execute(command: String) -> CommandResult: + return await _PARENT_COMMAND_RUNNER._handle_execute(command) func input(out: String = "") -> String: - return await _PARENT_PROCESS._handle_input(self, out) + return await _PARENT_COMMAND_RUNNER._handle_input(self, out) -func output(out, append_new_line: bool = true) -> void: - _PARENT_PROCESS._handle_output(str(out), append_new_line) +func output(out: Variant, append_new_line: bool = true) -> void: + _PARENT_COMMAND_RUNNER._handle_output(str(out), append_new_line) func get_ui_handler() -> GDShellUIHandler: - return _PARENT_PROCESS._handle_get_ui_handler() + return _PARENT_COMMAND_RUNNER._handle_get_ui_handler() func get_ui_handler_rich_text_label() -> RichTextLabel: - return _PARENT_PROCESS._handle_get_ui_handler_rich_text_label() + return _PARENT_COMMAND_RUNNER._handle_get_ui_handler_rich_text_label() + + +func _get_command_name() -> String: + @warning_ignore("unsafe_method_access") + return get_script().get_path().get_file().get_basename() + + +func _get_command_auto_aliases() -> Dictionary: + return {} func _get_manual() -> String: @@ -55,16 +66,16 @@ func _get_manual() -> String: -Override the [b]_get_manual()[/b] function for a custom manual page. """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) -static func argv_parse_options(argv: Array, strip_name_dashes: bool = false, next_arg_as_value: bool = false) -> Dictionary: +static func argv_parse_options(argv: Array[String], strip_name_dashes: bool = false, next_arg_as_value: bool = false) -> Dictionary: var options: Dictionary = {} - for i in argv.size(): + for i: int in argv.size(): if argv[i][0] == "-": var option_name: String = argv[i].get_slice("=", 0).lstrip("-") if strip_name_dashes else argv[i].get_slice("=", 0) var option_value: String = argv[i].get_slice("=", 1) if "=" in argv[i] else "" diff --git a/addons/gdshell/scripts/gdshell_command.gd.uid b/addons/gdshell/scripts/gdshell_command.gd.uid new file mode 100644 index 0000000..fee7be1 --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command.gd.uid @@ -0,0 +1 @@ +uid://b1ixb01gw3wcg diff --git a/addons/gdshell/scripts/gdshell_command_db.gd b/addons/gdshell/scripts/gdshell_command_db.gd index cbd0f59..f6cea56 100644 --- a/addons/gdshell/scripts/gdshell_command_db.gd +++ b/addons/gdshell/scripts/gdshell_command_db.gd @@ -7,104 +7,145 @@ var _commands: Dictionary = {} var _aliases: Dictionary = {} -func add_command(path: String) -> bool: - var name_and_auto_aliases: Dictionary = get_command_name_and_auto_aliases(path) +func has_command(command: String) -> bool: + return command in _commands + + +## Returns the command name on success or empty String on failure +func add_command(path: String) -> String: + var name_and_auto_aliases: Dictionary = GDShellCommandDB.get_file_command_name_and_auto_aliases(path) if name_and_auto_aliases.is_empty(): - return false + return "" _commands[name_and_auto_aliases["name"]] = path _aliases.merge(name_and_auto_aliases["aliases"], true) - return true + return name_and_auto_aliases["name"] func add_commands_in_directory(path: String, recursive: bool = true) -> void: - for command in get_command_file_paths_in_directory(path, recursive): + for command in GDShellCommandDB.get_command_file_paths_in_directory(path, recursive): + @warning_ignore("return_value_discarded") add_command(command) -func remove_command(command_name: String) -> bool: - return _commands.erase(command_name) +func remove_command(command_name: String) -> void: + @warning_ignore("return_value_discarded") + _commands.erase(command_name) +## Returns command path if the command is registered. Returns empty String if not. func get_command_path(command_name: String) -> String: return _commands.get(command_name, "") func get_all_command_names() -> Array[String]: - # This is required as GD4 doesn't allow upcast from Array -> Array[String] + # This is required as GD4 doesn't allow upcast from Array to Array[String] # see: https://www.reddit.com/r/godot/comments/10rqh9g/problem_with_typed_arrays_since_40_beta_17/ - # see: https://docs.godotengine.org/en/latest/classes/class_dictionary.html#class-dictionary-method-keys - var keys: Array[String] - for key in _commands.keys(): - if key is String: - keys.append(key) + var names: Array[String] = [] + names.assign(_commands.keys()) + return names - return keys +func get_all_commands() -> Dictionary: + return _commands.duplicate() -func add_alias(alias: String, command: String) -> bool: - # This prevents the stupidest cyclic dependency, but the aliases can still be locked into a cycle - # TODO: Somehow detect the cyclic alias/command dependency and return an error? - if alias == command: - return false + +func has_alias(alias: String) -> bool: + return alias in _aliases + + +func add_alias(alias: String, command: String) -> void: _aliases[alias] = command - return true -func remove_alias(alias: String) -> bool: - return _aliases.erase(alias) +func get_alias_value(alias: String) -> String: + return _aliases.get(alias, "") + + +func remove_alias(alias: String) -> void: + @warning_ignore("return_value_discarded") + _aliases.erase(alias) func get_all_aliases() -> Dictionary: return _aliases.duplicate() -static func get_file_paths_in_directory(path: String, recursive: bool = true) -> Array[String]: +static func _get_file_paths_in_directory(path: String, recursive: bool = true) -> Array[String]: var paths: Array[String] = [] var dir: DirAccess = DirAccess.open(path) - if dir != null: - dir.list_dir_begin() + if dir == null: + push_error("[GDShell] Cannot get file paths in directory \"%s\" - %s." % [path, DirAccess.get_open_error()]) + return [] + + var err: int = dir.list_dir_begin() + if err: + push_error("[GDShell] Cannot get file paths in directory \"%s\" - %s." % [path, error_string(err)]) + return [] + path = dir.get_next() + while path: + if dir.current_is_dir(): + if recursive: + paths.append_array(_get_file_paths_in_directory(dir.get_current_dir().path_join(path), true)) + else: + paths.append(dir.get_current_dir().path_join(path)) path = dir.get_next() - while path: - if dir.current_is_dir(): - if recursive: - paths.append_array(get_file_paths_in_directory(dir.get_current_dir().path_join(path), true)) - else: - paths.append(dir.get_current_dir().path_join(path)) - path = dir.get_next() - dir.list_dir_end() - if paths.is_empty(): - push_warning( - "[GDShell] No commands found in directory. Check the'GDShellCommandDB.get_file_paths_from_directory() argument'" - ) + dir.list_dir_end() + return paths static func get_command_file_paths_in_directory(path: String, recursive: bool = true) -> Array[String]: - return get_file_paths_in_directory(path, recursive).filter(func(x): return is_file_gdshell_command(x)) + return _get_file_paths_in_directory(path, recursive).filter(GDShellCommandDB.is_file_gdshell_command) static func is_file_gdshell_command(path: String) -> bool: var res: Resource = ResourceLoader.load(path, "GDScript") if not res is GDScript: return false - - var script: Object = (res as GDScript).new() - if not script is GDShellCommand: - return false - - return true + return _is_script_gdshell_command(res as GDScript) -static func get_command_name_and_auto_aliases(path: String) -> Dictionary: +static func _is_script_gdshell_command(script: Script) -> bool: + # HACK: + # This is super error prone. If the name of GDShellCommand script ever changes, this breaks. + # I haven't found or figured out a better workaround so this is just to make it work. + return script.get_script_property_list().any( + func(property: Dictionary) -> bool: + return str(property["name"]) == "gdshell_command.gd" + ) + + +func get_command_name_and_auto_aliases(command: String) -> Dictionary: + return ( + GDShellCommandDB.get_file_command_name_and_auto_aliases(_commands[command]) if command in _commands + else {"name": "", "aliases": []} + ) + + +static func get_file_command_name_and_auto_aliases(path: String) -> Dictionary: var out: Dictionary = {"name": "", "aliases": []} - var res: Resource = ResourceLoader.load(path, "GDScript") - if not res is GDScript: + var command: GDShellCommand = get_file_gdshell_command_instance(path) + if command == null: return out + out["name"] = command._get_command_name() + out["aliases"] = command._get_command_auto_aliases() + return out + + +# This is a if monster, but it just checks if the command really exists +func get_gdshell_command_instance(command_name: String) -> GDShellCommand: + var command_script_path: String = get_command_path(command_name) + if command_script_path.is_empty(): + return null + return GDShellCommandDB.get_file_gdshell_command_instance(command_script_path) + + +static func get_file_gdshell_command_instance(path: String) -> GDShellCommand: + if not is_file_gdshell_command(path): + return null - var script: Object = (res as GDScript).new() - if not script is GDShellCommand: - return out + var command_script: Resource = ResourceLoader.load(path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) + if command_script == null: + return null - out["name"] = (script as GDShellCommand).COMMAND_NAME - out["aliases"] = (script as GDShellCommand).COMMAND_AUTO_ALIASES - return out + return (command_script as GDScript).new() as GDShellCommand diff --git a/addons/gdshell/scripts/gdshell_command_db.gd.uid b/addons/gdshell/scripts/gdshell_command_db.gd.uid new file mode 100644 index 0000000..363d2b4 --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command_db.gd.uid @@ -0,0 +1 @@ +uid://c1lh7iliyrra1 diff --git a/addons/gdshell/scripts/gdshell_command_parser.gd b/addons/gdshell/scripts/gdshell_command_parser.gd deleted file mode 100644 index 1285249..0000000 --- a/addons/gdshell/scripts/gdshell_command_parser.gd +++ /dev/null @@ -1,395 +0,0 @@ -@icon("res://addons/gdshell/icon.png") -class_name GDShellCommandParser -extends RefCounted - - -enum TokenType { - ERROR, - TOKEN_SEQUENCE, - TEXT, - UNTERMINATED_TEXT, - SEPARATOR, - PIPE, - AND, - OR, - NOT, - BACKGROUND, - SEQUENCE, -} - -enum ParserBlockType { - COMMAND, - BACKGROUND, - NOT, - PIPE, - AND, - OR, -} - -enum ParserResultStatus { - OK, - UNTERMINATED, - ERROR, -} - -# status - ParserResultStatus: -# OK - result is an Array of Dictionaries (ParserBlockTypes) -# ERROR - result is the error token Dictionary -# UNTERMINATED - result is an empty Array - new input is necessary - - -static func parse(input: String, command_db: GDShellCommandDB) -> Dictionary: - var tokens: Array[Dictionary] = tokenize(input, command_db) - - if tokens.is_empty(): - return {"status": ParserResultStatus.OK, "result": []} - - if tokens[-1]["type"] == TokenType.UNTERMINATED_TEXT: - return {"status": ParserResultStatus.UNTERMINATED, "result": []} - - var command_sequence: Array[Dictionary] = [] - var command_construction_temp: Array[String] = [] - - for i in tokens.size(): - match tokens[i]["type"]: - TokenType.ERROR: - return {"status": ParserResultStatus.ERROR, "result": tokens[i]} - - TokenType.TEXT: - command_construction_temp.push_back(tokens[i]["content"]) - - TokenType.PIPE: - var operator_validation: Dictionary = _validate_operator(tokens, i, true, true) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - command_sequence.push_back({"type": ParserBlockType.PIPE}) - - TokenType.OR: - var operator_validation: Dictionary = _validate_operator(tokens, i, true, true) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - command_sequence.push_back({"type": ParserBlockType.OR}) - - TokenType.AND: - var operator_validation: Dictionary = _validate_operator(tokens, i, true, true) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - command_sequence.push_back({"type": ParserBlockType.AND}) - - TokenType.NOT: - var operator_validation: Dictionary = _validate_operator(tokens, i, false, true) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - command_sequence.push_back({"type": ParserBlockType.NOT}) - - TokenType.BACKGROUND: - var operator_validation: Dictionary = _validate_operator(tokens, i, true, false) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back({"type": ParserBlockType.BACKGROUND}) - command_sequence.push_back(command["data"]) - - TokenType.SEQUENCE: - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - - _: - return { - "status": ParserResultStatus.ERROR, - "result": - { - "error": - { - "char": tokens[i]["start_char"], - "error": "Unknown token", - } - } - } - - # Empty the `command_construction_temp` - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - command_sequence.push_back(command["data"]) - - command_sequence = command_sequence.filter(func(x: Dictionary): return not x.is_empty()) - - return {"status": ParserResultStatus.OK, "result": command_sequence} - - -static func _construct_command(from: Array[String], command_db: GDShellCommandDB) -> Dictionary: - if from.size() == 0: - # Empty commands will be filtered out - return { - "status": ParserResultStatus.OK, - "data": {}, - } - - var command_path: String = command_db.get_command_path(from[0]) - if command_path.is_empty(): - return { - "status": ParserResultStatus.ERROR, - "result": {"error": {"char": 0, "error": "Unknown command: %s" % from[0]}}, - } - - return { - "status": ParserResultStatus.OK, - "data": - { - "type": ParserBlockType.COMMAND, - "data": - { - "command": command_path, - "params": - { - "argv": from.duplicate(true), - "data": null, - } - } - } - } - - -# `from` - token array ; `at` - index of the operator -# `lect`, `right` - determines if the oprator mush have left or RIGHT operands -static func _validate_operator(from: Array[Dictionary], at: int, left: bool, right: bool) -> Dictionary: - if left: - if not at > 0: - return { - "status": ParserResultStatus.ERROR, - "result": - { - "error": - { - "char": from[at]["start_char"], - "error": "Missing operand", - } - } - } - if from[at - 1]["type"] != TokenType.TEXT: - if from[at - 1]["type"] == TokenType.BACKGROUND and from[at]["type"] == TokenType.BACKGROUND: - return { - "status": ParserResultStatus.ERROR, - "result": - { - "error": - { - "char": from[at]["start_char"], - "error": "Missing operand", - } - } - } - - if right: - if not at < from.size() - 1: - return { - "status": ParserResultStatus.ERROR, - "result": - { - "error": - { - "char": from[at]["start_char"] + from[at]["consumed"], - "error": "Missing operand", - } - } - } - if from[at + 1]["type"] != TokenType.TEXT: - if ( - from[at + 1]["type"] != TokenType.NOT - or (from[at + 1]["type"] == TokenType.NOT and from[at]["type"] == TokenType.NOT) - ): - return { - "status": ParserResultStatus.ERROR, - "result": - { - "error": - { - "char": from[at]["start_char"] + from[at]["consumed"], - "error": "Missing operand", - } - } - } - return {"status": ParserResultStatus.OK} - - -static func tokenize(input: String, command_db: GDShellCommandDB) -> Array[Dictionary]: - var tokens: Array[Dictionary] = [] - var current: int = 0 - - while current < input.length(): - match input[current]: - " ": - tokens.push_back(_tokenize_separator(input, current)) - "|": - tokens.push_back(_tokenize_pipe_or(input, current)) - "&": - tokens.push_back(_tokenize_background_and(input, current)) - ";": - tokens.push_back(_tokenize_sequence(input, current)) - '"', "'": - tokens.push_back(_tokenize_quoted_text(input, current)) - "!": - tokens.push_back(_tokenize_not(input, current)) - _: - tokens.push_back(_tokenize_text(input, current)) - - if tokens[-1]["type"] == TokenType.ERROR: - return [tokens[-1]] - - current += tokens[-1]["consumed"] - - tokens = _remove_separator_tokens(tokens) - return _unalias_tokens(tokens, command_db) - - -static func _token(type: TokenType, start_char: int, consumed: int, content: String = "", error: Dictionary = {}) -> Dictionary: - return { - "type": type, - "start_char": start_char, - "consumed": consumed, - "content": content, - "error": error, - } - - -static func _tokenize_quoted_text(input: String, current: int) -> Dictionary: - var start_char = current - var content: String = "" - var quote_type: String = input[current] - current += 1 - - while true: - if current >= input.length(): - return _token(TokenType.UNTERMINATED_TEXT, start_char, content.length() + 1, content.c_unescape()) # accounts for the starting quote - - if input[current] == quote_type: - if input[max(0, current - 1)] != "\\": - return _token(TokenType.TEXT, start_char, content.length() + 2, content.c_unescape()) # accounts for the starting and ending quotes - - content += input[current] - current += 1 - - return {} - - -static func _tokenize_text(input: String, current: int) -> Dictionary: - var start_char = current - var content: String = "" - - while true: - if current == input.length() or input[current] in [" ", "&"]: - return _token( - TokenType.TEXT, - start_char, - content.length(), - content.c_unescape() - ) - - if input[current] in [";", "|", '"', "'", "!"]: - return _token( - TokenType.ERROR, - start_char, - content.length() + 1, - content, - {"char": current, "error": "Unexpected token"} - ) - - content += input[current] - current += 1 - - return {} - - -static func _tokenize_separator(_input: String, current: int) -> Dictionary: - return _token(TokenType.SEPARATOR, current, 1, " ") - - -static func _tokenize_pipe_or(input: String, current: int) -> Dictionary: - if current < input.length() - 1 and input[current + 1] == "|": - return _token(TokenType.OR, current, 2, "||") - return _token(TokenType.PIPE, current, 1, "|") - - -static func _tokenize_background_and(input: String, current: int) -> Dictionary: - if current < input.length() - 1 and input[current + 1] == "&": - return _token(TokenType.AND, current, 2, "&&") - return _token(TokenType.BACKGROUND, current, 1, "&") - - -static func _tokenize_not(_input: String, current: int) -> Dictionary: - return _token(TokenType.NOT, current, 1, "!") - - -static func _tokenize_sequence(_input: String, current: int) -> Dictionary: - return _token(TokenType.SEQUENCE, current, 1, ";") - - -static func _unalias_tokens(tokens: Array[Dictionary], command_db: GDShellCommandDB) -> Array[Dictionary]: - if tokens.size() == 0: - return tokens - if tokens[-1]["type"] == TokenType.UNTERMINATED_TEXT: - return tokens - - tokens = _remove_separator_tokens(tokens) - - # Replace aliasable token by a token sequence representing the alias - for i in tokens.size(): - if tokens[i]["type"] != TokenType.TEXT: - continue - if i > 0 and (tokens[i - 1]["type"] == TokenType.TEXT or tokens[i - 1]["type"] == TokenType.TOKEN_SEQUENCE): - continue - # Alias is found for the aliasable token - if tokens[i]["content"] in command_db._aliases.keys(): - tokens[i] = { - "type": TokenType.TOKEN_SEQUENCE, - "content": tokenize(command_db._aliases[tokens[i]["content"]], command_db) - } - - # Insert token sequences as tokens into the `tokens` array - while tokens.any(func(x): return x["type"] == TokenType.TOKEN_SEQUENCE): - for i in tokens.size(): - if tokens[i]["type"] == TokenType.TOKEN_SEQUENCE: - var token_sequence: Dictionary = tokens.pop_at(i) - @warning_ignore("unsafe_method_access") - for ii in token_sequence["content"].size(): - tokens.insert(i + ii, token_sequence["content"][ii]) - break # Break because the indexing has changed because of the inserting - - return tokens - - -static func _remove_separator_tokens(tokens: Array[Dictionary]) -> Array[Dictionary]: - return tokens.filter(func(x): return x["type"] != TokenType.SEPARATOR) diff --git a/addons/gdshell/scripts/gdshell_command_runner.gd b/addons/gdshell/scripts/gdshell_command_runner.gd index 6ac2259..ed80fef 100644 --- a/addons/gdshell/scripts/gdshell_command_runner.gd +++ b/addons/gdshell/scripts/gdshell_command_runner.gd @@ -1,142 +1,98 @@ @icon("res://addons/gdshell/icon.png") -class_name GDShellCommandRunner +#class_name GDShellCommandRunner extends Node - -# Command execution flags -const F_EXECUTE_CONDITION_MET: int = 1 << 0 -const F_PIPE_PREVIOUS: int = 1 << 1 -const F_BACKGROUND: int = 1 << 2 -const F_NEGATED: int = 1 << 3 - -var _PARENT_GDSHELL: GDShellMain - var _background_commands: Array[GDShellCommand] = [] -var _is_running_command: bool = false -func execute(command_sequence: Dictionary) -> Dictionary: - if command_sequence["status"] != GDShellCommandParser.ParserResultStatus.OK: - return { - "error": 1, - "error_string": 'Cannot execute command sequence. See "data" for the command_sequence', - "data": command_sequence, - } + +class RunnerResult extends RefCounted: + enum Status { + OK, + COMPILE_ERROR, + VALIDATION_ERROR, + RUNTIME_ERROR, + } + + var status: Status + var error_description: String + var result: GDShellCommand.CommandResult + var error_index: int + var error_length: int - _is_running_command = true - var current_token: int = 0 - var current_command_flags: int = F_EXECUTE_CONDITION_MET - var last_command_result: Dictionary = GDShellCommand.DEFAULT_COMMAND_RESULT + func _init(_status: Status, _error_description: String, _result: GDShellCommand.CommandResult, _error_index: int, _error_length: int) -> void: + status = _status + error_description = _error_description + result = _result + error_index = _error_index + error_length = _error_length + + +func execute(expression: String, command_db: GDShellCommandDB) -> RunnerResult: + var compiler_result: GDShellExpressionCompiler.CompilerResult = GDShellExpressionCompiler.compile(expression) + if compiler_result.status != GDShellExpressionCompiler.CompilerResult.Status.OK: + return RunnerResult.new(RunnerResult.Status.COMPILE_ERROR, compiler_result.error_description, null, -1, -1) - @warning_ignore("unsafe_method_access") - while current_token < command_sequence["result"].size(): - match command_sequence["result"][current_token]["type"]: - GDShellCommandParser.ParserBlockType.COMMAND: - var command: String = command_sequence["result"][current_token]["data"]["command"] - var params: Dictionary = command_sequence["result"][current_token]["data"]["params"] - - if not current_command_flags & F_EXECUTE_CONDITION_MET: - current_command_flags = F_EXECUTE_CONDITION_MET - continue - - if current_command_flags & F_PIPE_PREVIOUS: - params["data"] = last_command_result["data"] - - if current_command_flags & F_BACKGROUND: - last_command_result = GDShellCommand.DEFAULT_COMMAND_RESULT - _execute_command(command, params) - else: - last_command_result = await _execute_command(command, params) - - if current_command_flags & F_NEGATED: - last_command_result["error"] = 0 if last_command_result["error"] else 1 - - current_command_flags = F_EXECUTE_CONDITION_MET - - GDShellCommandParser.ParserBlockType.BACKGROUND: - current_command_flags |= F_BACKGROUND - - GDShellCommandParser.ParserBlockType.NOT: - current_command_flags |= F_NEGATED - - GDShellCommandParser.ParserBlockType.PIPE: - current_command_flags |= F_PIPE_PREVIOUS - - GDShellCommandParser.ParserBlockType.AND: - if last_command_result["error"]: - current_command_flags ^= F_EXECUTE_CONDITION_MET - else: - current_command_flags |= F_EXECUTE_CONDITION_MET - - GDShellCommandParser.ParserBlockType.OR: - if last_command_result["error"]: - current_command_flags |= F_EXECUTE_CONDITION_MET - else: - current_command_flags ^= F_EXECUTE_CONDITION_MET - - current_token += 1 + return execute_compiled(compiler_result.result, command_db) + + +func execute_compiled(compiled_expression: Dictionary, command_db: GDShellCommandDB) -> RunnerResult: + if not GDShellExpressionCompiler.is_expression_valid(compiled_expression, false, command_db): + return RunnerResult.new(RunnerResult.Status.VALIDATION_ERROR, "invalid compiled expression", null, -1, -1) + return _execute(compiled_expression, command_db) + + +func _execute(expression: Dictionary, command_db: GDShellCommandDB) -> RunnerResult: + return null + + +func _execute_operator(operator_expression: Dictionary, command_db: GDShellCommandDB) -> RunnerResult: + match operator_expression["operator"]: + "!": + var right_operand_result: RunnerResult = _execute(operator_expression["right"], command_db) + if right_operand_result.status == OK: + right_operand_result.result.err = FAILED if right_operand_result.result.err == OK else OK + return right_operand_result + "&": + pass + "|": + pass + "||": + pass + "&&": + pass + ";": + @warning_ignore("redundant_await") + await _execute(operator_expression["left"], command_db) + return await _execute(operator_expression["right"], command_db) - _is_running_command = false - return last_command_result + return null -func _execute_command(path: String, params: Dictionary, in_background: bool = false) -> Dictionary: - @warning_ignore("unsafe_method_access", "unsafe_cast") - var command: GDShellCommand = ResourceLoader.load(path, "GDScript").new() as GDShellCommand - add_child(command) - command._PARENT_PROCESS = self +func _execute_command(command_expression: Dictionary, piped_data: Variant, command_db: GDShellCommandDB, in_background: bool = false) -> RunnerResult: + # Create command + var command: GDShellCommand = command_db.get_gdshell_command_instance(command_expression["name"]) + if command == null: + return RunnerResult.new(RunnerResult.Status.RUNTIME_ERROR, "cannot create a command instance", null, -1, -1) + # Setup command + command.name = "GDShellCommand: " + command._get_command_name() + add_child(command, true) if in_background: + command.name += " (in background)" _background_commands.append(command) - - @warning_ignore("redundant_await") - var result = await command._main(params["argv"], params["data"]) - - if typeof(result) != TYPE_DICTIONARY: - push_error("[GDShell] The '%s' command does not return a value of TYPE_DICTIONARY.\n'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead." - % params["argv"][0] - ) - # This assert statement acts as a hard error in the editor - assert( - typeof(result) == TYPE_DICTIONARY, - """[GDShell] The command does not return a value of TYPE_DICTIONARY. - 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead. - See the Errors for more information about the failing command.""" - ) - result = GDShellCommand.DEFAULT_COMMAND_RESULT + # Run command + var command_result: GDShellCommand.CommandResult + if in_background: + command._main(command_expression["args"], piped_data) + command_result = GDShellCommand.CommandResult.new() else: - @warning_ignore("unsafe_method_access") - result.merge(GDShellCommand.DEFAULT_COMMAND_RESULT) - - command.queue_free() + @warning_ignore("redundant_await") # We don't know if the user override will have await + command_result = await command._main(command_expression["args"], piped_data) + # TODO - does this work with both background and normal commands? Won't background commands be freed prematurely? + # Cleanup command _background_commands.erase(command) + command.queue_free() - return result - - -############################################## -# GDShellCommand-GDShell interface functions # -############################################## - - -func _handle_execute(command: String) -> Dictionary: - return await _PARENT_GDSHELL.execute(command) - - -func _handle_input(command: GDShellCommand, out: String) -> String: - if command in _background_commands: - return "" - return await _PARENT_GDSHELL._request_input_from_ui_handler(out) - - -func _handle_output(out: String, append_new_line: bool = true) -> void: - _PARENT_GDSHELL._request_output_from_ui_handler(out, append_new_line) - - -func _handle_get_ui_handler() -> GDShellUIHandler: - return _PARENT_GDSHELL.get_ui_handler() - - -func _handle_get_ui_handler_rich_text_label() -> RichTextLabel: - return _PARENT_GDSHELL.get_ui_handler_rich_text_label() + return RunnerResult.new(RunnerResult.Status.OK, "OK", command_result, -1, -1) diff --git a/addons/gdshell/scripts/gdshell_command_runner.gd.uid b/addons/gdshell/scripts/gdshell_command_runner.gd.uid new file mode 100644 index 0000000..b28223b --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command_runner.gd.uid @@ -0,0 +1 @@ +uid://dai6np6gwb82 diff --git a/addons/gdshell/scripts/gdshell_command_runner_old.gd b/addons/gdshell/scripts/gdshell_command_runner_old.gd new file mode 100644 index 0000000..b4536ae --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command_runner_old.gd @@ -0,0 +1,202 @@ +@icon("res://addons/gdshell/icon.png") +class_name GDShellCommandRunner +extends Node + + +# Command execution flags +const F_EXECUTE_CONDITION_MET: int = 1 +const F_PIPE_PREVIOUS: int = 2 +const F_BACKGROUND: int = 4 +const F_NEGATED: int = 8 + +var _PARENT_GDSHELL: GDShellMain + +var _background_commands: Array[GDShellCommand] = [] + + +func _init() -> void: + name = "GDShellCommandRunner - " + + +#func execute(parser_result: GDShellCommandParser.ParserResult, piped_result: GDShellCommand.CommandResult=null) -> GDShellCommand.CommandResult: + #if parser_result.status != GDShellCommandParser.ParserResult.Status.OK: + #push_error("[GDShell] Attempted to run invalid GDShellCommandParser.ParserResult.") + #return null + #if parser_result.tokens.is_empty(): + #push_error("[GDShell] Attempted to run an empty input.") + #return null + #if parser_result.command_db == null: + #push_error("[GDShell] Attempted to run a command, but GDShellCommandParser.ParserResult.command_db is null.") + #return null + # + #var command_execution_flags: int = F_EXECUTE_CONDITION_MET + #var last_command_result: GDShellCommand.CommandResult = piped_result + # + #var current_token_index: int = 0 + #while current_token_index < parser_result.tokens.size(): + #match parser_result.tokens[current_token_index].type: + # + #GDShellCommandParser.Token.Type.WORD: + #var next_non_word_token_index: int = _get_next_non_word_token_index(parser_result.tokens, current_token_index) + #var executed_command_result: GDShellCommand.CommandResult = await _execute_words( + #parser_result.tokens.slice(current_token_index, next_non_word_token_index), + #parser_result.command_db, + #last_command_result, + #command_execution_flags + #) + # + #if executed_command_result != null: + #last_command_result = executed_command_result + # + #command_execution_flags &= F_EXECUTE_CONDITION_MET + #current_token_index = next_non_word_token_index + # + #GDShellCommandParser.Token.Type.OPERATOR_PIPE: + #pass + # + #GDShellCommandParser.Token.Type.OPERATOR_AND: + #if not last_command_result.err: + #command_execution_flags |= F_BACKGROUND + #else: + #command_execution_flags ^= F_BACKGROUND + # + #GDShellCommandParser.Token.Type.OPERATOR_OR: + #if last_command_result.err: + #command_execution_flags |= F_BACKGROUND + #else: + #command_execution_flags ^= F_BACKGROUND + # + #GDShellCommandParser.Token.Type.OPERATOR_NOT: + #command_execution_flags |= F_NEGATED + # + #GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: + #command_execution_flags |= F_BACKGROUND + # + #GDShellCommandParser.Token.Type.OPERATOR_SEQUENCE: + #command_execution_flags |= F_EXECUTE_CONDITION_MET + # + #GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: + #var parentheses_content: Array[GDShellCommandParser.Token] = _get_parenthesis_inner_tokens(parser_result.tokens, current_token_index) + #var executed_parentheses_result: GDShellCommand.CommandResult = await execute( + #GDShellCommandParser.ParserResult.new( + #GDShellCommandParser.ParserResult.Status.OK, + #"", + #parentheses_content, + #parser_result.command_db + #) + #) + #if executed_parentheses_result == null: + #return null + # + #last_command_result = executed_parentheses_result + # + #GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: + #pass # Do nothing (don't delete this. This token should not trigger an error) + # + #var token_type: + #push_error("[GDShell] GDShellCommandRunner encountered an unexpected '%s' token." % str(GDShellCommandParser.Token.Type.find_key(token_type))) + #return null + # + #current_token_index += 1 + # + #return last_command_result +# +# +#func _execute_words(tokens: Array[GDShellCommandParser.Token], command_db: GDShellCommandDB, last_result: GDShellCommand.CommandResult, command_execution_flags: int) -> GDShellCommand.CommandResult: + #if not command_execution_flags & F_EXECUTE_CONDITION_MET: + #return null + # + #var command_result: GDShellCommand.CommandResult = await _execute_command( + #tokens, + #command_db, + #last_result if command_execution_flags & F_PIPE_PREVIOUS else null, + #command_execution_flags & F_BACKGROUND + #) + # + #if command_execution_flags & F_NEGATED: + #command_result.err = OK if command_result.err else FAILED + # + #return command_result +# + +#func _execute_command(words: Array[GDShellCommandParser.Token], command_db: GDShellCommandDB, piped_result: GDShellCommand.CommandResult=null, in_background: bool=false) -> GDShellCommand.CommandResult: + ## Get argv + ## Fancy way to make the Array typed + #var argv: Array = Array(words.map( + #func(token: GDShellCommandParser.Token) -> String: + #return token.content + #), TYPE_STRING, "", null) + # + ## Create command instance + #var command: GDShellCommand = command_db.get_gdshell_command_instance(argv[0]) + #if command == null: + #return null + # + ## Set up command + #command._PARENT_COMMAND_RUNNER = self + #command.name = "GDShellCommand: " + command._get_command_name() + #add_child(command, true) + #if in_background: + #command.name += " (in background)" + #_background_commands.append(command) + # + ## Run command + #@warning_ignore("redundant_await") # We don't know if the user override will have await + #var command_result: GDShellCommand.CommandResult = await command._main(argv, piped_result) + # + ## Cleanup command + #_background_commands.erase(command) + #command.queue_free() + #return command_result +# +# +#func _get_next_non_word_token_index(tokens: Array[GDShellCommandParser.Token], starting_from: int) -> int: + #var next_non_word_token_index: int = starting_from + 1 + #while next_non_word_token_index < tokens.size(): + #if tokens[next_non_word_token_index].type != GDShellCommandParser.Token.Type.WORD: + #break + #next_non_word_token_index += 1 + #return next_non_word_token_index +# +# +#func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: + #var current: int = opening_parenthesis_index + #var parenthesis_level: int = 1 + # + #while current < tokens.size(): + #if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: + #parenthesis_level += 1 + #elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: + #parenthesis_level -= 1 + #if parenthesis_level == 0: + #return current + #return -1 +# +# +#func _get_parenthesis_inner_tokens(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> Array[GDShellCommandParser.Token]: + #var matching_parenthesis_index: int = _find_matching_parenthesis_index(tokens, opening_parenthesis_index) + #if matching_parenthesis_index == -1: + #return [] + #return tokens.slice(opening_parenthesis_index + 1, matching_parenthesis_index) + + +func _handle_execute(command: String) -> GDShellCommand.CommandResult: + return await _PARENT_GDSHELL.execute(command) + + +func _handle_input(command: GDShellCommand, out: String) -> String: + if command in _background_commands: + return "" + return await _PARENT_GDSHELL._request_input_from_ui_handler(out) + + +func _handle_output(out: String, append_new_line: bool = true) -> void: + _PARENT_GDSHELL._request_output_from_ui_handler(out, append_new_line) + + +func _handle_get_ui_handler() -> GDShellUIHandler: + return _PARENT_GDSHELL.get_ui_handler() + + +func _handle_get_ui_handler_rich_text_label() -> RichTextLabel: + return _PARENT_GDSHELL.get_ui_handler_rich_text_label() diff --git a/addons/gdshell/scripts/gdshell_command_runner_old.gd.uid b/addons/gdshell/scripts/gdshell_command_runner_old.gd.uid new file mode 100644 index 0000000..c96485b --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command_runner_old.gd.uid @@ -0,0 +1 @@ +uid://sp64qq7g876c diff --git a/addons/gdshell/scripts/gdshell_main.gd b/addons/gdshell/scripts/gdshell_main.gd index 3472598..bbf1f95 100644 --- a/addons/gdshell/scripts/gdshell_main.gd +++ b/addons/gdshell/scripts/gdshell_main.gd @@ -3,163 +3,211 @@ class_name GDShellMain extends Node -signal _input_submitted(input: String) - - -var UI_TOGGLE_ACTION: String = ProjectSettings.get_setting( - GDShellEditorPlugin.UI_TOGGLE_ACTION, - GDShellEditorPlugin.UI_TOGGLE_ACTION_DEFAULT -) +signal _requested_input_submitted(input: String) var command_runner: GDShellCommandRunner var command_db: GDShellCommandDB var ui_handler: GDShellUIHandler # Internal helper variables -var _is_command_awaiting_input: bool = false +var _ui_handler_canvas_layer: CanvasLayer var _input_buffer: String = "" +var _input_requested: bool = false -func _ready() -> void: - if get_parent() == get_tree().root: # is singleton - setup_as_singleton() - else: - push_warning("GDShellMain was instanced directly so don't forget to set it up manually. For reference checkout GDShellMain.setup_as_singleton()") +func _ready() -> void: + _ui_handler_canvas_layer = CanvasLayer.new() + _ui_handler_canvas_layer.layer = 100 + add_child(_ui_handler_canvas_layer) + + setup_with_default_values() + + var result = GDShellExpressionCompiler.compile("'a''a'") + #var result = GDShellExpressionCompiler.compile("a && b ; (c | d)") + printerr(GDShellExpressionCompiler.is_expression_valid(result.result)) + print(JSON.stringify(result.result, "\t", false)) + #print(result.result) + return + + #if "autorun" in command_db.get_all_command_names(): + #@warning_ignore("return_value_discarded") + #execute("autorun") -func setup_as_singleton() -> void: - setup_command_runner() +func setup_with_default_values() -> void: + # GDShellCommandRunner + set_command_runner(GDShellCommandRunner.new(), true) - setup_command_db( - ProjectSettings.get_setting( - GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES, - GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES_DEFAULT - ) - ) + # GDShellCommandDB + var database: GDShellCommandDB = GDShellCommandDB.new() + for directory: String in ProjectSettings.get_setting( + GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES, + GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES_DEFAULT + ): + database.add_commands_in_directory(directory) + set_command_db(database) - setup_ui_handler( - GDShellMain.load_ui_handler_from_path( - ProjectSettings.get_setting( + #GDShellUIHandler + set_ui_handler( + GDShellMain._get_ui_handler_instance_from_path( + str(ProjectSettings.get_setting( GDShellEditorPlugin.UI_SCENE_PATH, GDShellEditorPlugin.UI_SCENE_PATH_DEFAULT - ) + )) ), - true, - ProjectSettings.get_setting( - GDShellEditorPlugin.UI_CANVAS_LAYER, - GDShellEditorPlugin.UI_CANVAS_LAYER_DEFAULT - ) + true ) - - execute_autorun() -func setup_command_runner() -> void: - command_runner = GDShellCommandRunner.new() - command_runner._PARENT_GDSHELL = self - add_child(command_runner) +func set_command_db(new_command_db: GDShellCommandDB) -> void: + if new_command_db == null: + push_error("[GDShell] Attempted to set GDShellCommandDB, but null value was given.") + return + command_db = new_command_db -func setup_command_db(command_dir_paths: Array) -> void: - command_db = GDShellCommandDB.new() - - if ( - command_dir_paths.is_empty() - or command_dir_paths.all( - func(path): return typeof(path) != TYPE_STRING - ) - or not command_dir_paths.any( - func(path): return DirAccess.dir_exists_absolute(path) - ) - ): - push_error("[GDShell] No commands were loaded as there are no dir paths in 'Project/ProjectSettings/%s'" % GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES) - return - - for dir in command_dir_paths: - if typeof(dir) == TYPE_STRING: - command_db.add_commands_in_directory(dir) +func unset_command_db() -> void: + command_db = null -func setup_ui_handler(handler: GDShellUIHandler, add_as_child: bool = true, canvas_layer: int = 100) -> void: - ui_handler = handler - ui_handler._PARENT_GDSHELL = self - ui_handler.set_visible(false) +func set_command_runner(new_command_runner: GDShellCommandRunner, add_as_child: bool = true) -> void: + if new_command_runner == null: + push_error("[GDShell] Attempted to set GDShellCommandRunner, but null value was given.") + return + # if new command runner is the same as the onld one, do nothing unless it is requested to be reparented + if new_command_runner == command_runner: + if add_as_child and new_command_runner.get_parent() != self: + new_command_runner.reparent(self) + return + + unset_command_runner() if add_as_child: - var cl: CanvasLayer = CanvasLayer.new() - cl.layer = canvas_layer - cl.add_child(handler) - add_child(cl) + if new_command_runner.get_parent() != null: + push_error("[GDShell] Attempted to set GDShellCommandRunner, but runner already has a parent Node. Set 'add_as_child' to false if you wish to manage the runner yourself.") + return + add_child(new_command_runner) + + command_runner = new_command_runner + command_runner._PARENT_GDSHELL = self + + if command_runner.get_parent() == null: + push_warning("[GDShell] GDShellCommandRunner was set, but it has no parent. Make sure to give it a parent or remember to free it manually to prevent leaks if you wish to manage the runner yourself.") -func execute_autorun() -> void: - if "autorun" in command_db.get_all_command_names(): - @warning_ignore("return_value_discarded") - execute("autorun") +func unset_command_runner() -> void: + if ui_handler == null: + push_warning("[GDShell] Attempted to unset GDShellCommandRunner, but none was set.") + return + if command_runner.get_parent() == self: + command_runner.queue_free() + elif command_runner.get_parent() == null: + push_warning("[GDShell] Unset GDShellCommandRunner has no parent and was not freed. Remember to free it manually to prevent leaks.") + command_runner = null -func execute(command: String) -> Dictionary: - var command_sequence: Dictionary = GDShellCommandParser.parse(command, command_db) - if command_sequence["status"] == GDShellCommandParser.ParserResultStatus.OK: - return await command_runner.execute(command_sequence) - return command_sequence +func set_ui_handler(new_ui_handler: GDShellUIHandler, add_as_child: bool = true) -> void: + if new_ui_handler == null: + push_error("[GDShell] Attempted to set GDShellUIHandler, but null value was given.") + return + # if new handler is the same as the onld one, do nothing unless it is requested to be reparented + if new_ui_handler == ui_handler: + if add_as_child and new_ui_handler.get_parent() != _ui_handler_canvas_layer: + new_ui_handler.reparent(_ui_handler_canvas_layer) + return + + unset_ui_handler() + + if add_as_child: + if new_ui_handler.get_parent() != null: + push_error("[GDShell] Attempted to set GDShellUIHandler, but new_ui_handler already has a parent Node. Set 'add_as_child' to false if you wish to manage the handler yourself.") + return + _ui_handler_canvas_layer.add_child(new_ui_handler) + + ui_handler = new_ui_handler + ui_handler.set_visible(false) + if not ui_handler.input_submitted.is_connected(self._on_ui_handler_input_submitted): + @warning_ignore("return_value_discarded") + ui_handler.input_submitted.connect(self._on_ui_handler_input_submitted) + + if ui_handler.get_parent() == null: + push_warning("[GDShell] GDShellUIHandler was set, but it has no parent. Make sure to give it a parent or remember to free it manually to prevent leaks if you wish to manage the handler yourself.") -func get_ui_handler() -> GDShellUIHandler: - return ui_handler +func unset_ui_handler() -> void: + if ui_handler == null: + push_warning("[GDShell] Attempted to unset GDShellUIHandler, but none was set.") + return + + if ui_handler.input_submitted.is_connected(self._on_ui_handler_input_submitted): + ui_handler.input_submitted.disconnect(self._on_ui_handler_input_submitted) + if ui_handler.get_parent() == self: + ui_handler.queue_free() + elif ui_handler.get_parent() == null: + push_warning("[GDShell] Unset GDShellUIHandler has no parent and was not freed. Remember to free it manually to prevent leaks.") + ui_handler = null -func get_ui_handler_rich_text_label() -> RichTextLabel: - return ui_handler._get_output_rich_text_label() +func execute(command: String) -> GDShellCommand.CommandResult: + #var parser_result: GDShellCommandParser.ParserResult = GDShellCommandParser.parse(command, command_db) + #if parser_result.status == GDShellCommandParser.ParserResult.Status.OK: + #return await command_runner.execute(parser_result) + return null -func _request_input_from_ui_handler(out: String = "") -> String: - _is_command_awaiting_input = true - ui_handler._input_requested.emit(out) - return await _input_submitted +func _request_output_from_ui_handler(output: String, append_new_line: bool) -> void: + if ui_handler == null: + push_warning("[GDShell] No GDShellUIHandler is set. No output is outputted.") + return + ui_handler._output_requested(output) -func _request_output_from_ui_handler(output: String, append_new_line: bool) -> void: - ui_handler._output_requested.emit(output, append_new_line) +func _request_input_from_ui_handler(output: String = "") -> String: + if ui_handler == null: + push_warning("[GDShell] No GDShellUIHandler is set. Empty input is returned.") + return "" + ui_handler._input_requested(output) + return await _requested_input_submitted -func _submit_input(input: String) -> void: - if _is_command_awaiting_input: - _is_command_awaiting_input = false - _request_output_from_ui_handler(input, true) - _input_submitted.emit(input) +func _on_ui_handler_input_submitted(input: String) -> void: + if _input_requested: # The input is requested by a command. Do nothing and just forward it to it + _input_requested = false + _requested_input_submitted.emit(input) return _input_buffer += input - var command_sequence: Dictionary = GDShellCommandParser.parse(_input_buffer, command_db) - match command_sequence["status"]: - GDShellCommandParser.ParserResultStatus.OK: - _request_output_from_ui_handler( - (ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true - ) - _input_buffer = "" - await command_runner.execute(command_sequence) - ui_handler._input_requested.emit("") - GDShellCommandParser.ParserResultStatus.UNTERMINATED: - _request_output_from_ui_handler( - (ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true - ) - ui_handler._input_requested.emit("> ") - GDShellCommandParser.ParserResultStatus.ERROR: - _request_output_from_ui_handler( - ui_handler._get_input_prompt() + _input_buffer, true - ) - _input_buffer = "" - # TODO better error announcement - _request_output_from_ui_handler( - "[color=red]%s[/color]" % command_sequence["result"]["error"], true - ) - ui_handler._input_requested.emit("") - - -static func load_ui_handler_from_path(path: String) -> GDShellUIHandler: - @warning_ignore("unsafe_cast", "unsafe_method_access") - return load(path).instantiate() as GDShellUIHandler + #var parser_result: GDShellCommandParser.ParserResult = GDShellCommandParser.parse(_input_buffer, command_db) + #match parser_result.status: + #GDShellCommandParser.ParserResult.Status.OK: + #_request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) + #_input_buffer = "" + #await command_runner.execute(parser_result) + ##ui_handler._input_requested.emit("") + #GDShellCommandParser.ParserResult.Status.UNTERMINATED: + #_request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) + ##ui_handler._input_requested.emit("> ") + #GDShellCommandParser.ParserResult.Status.ERROR: + #_request_output_from_ui_handler(ui_handler._get_input_prompt() + _input_buffer, true) + #_input_buffer = "" + ## TODO better error announcement +## _request_output_from_ui_handler("[color=red]%s[/color]" % parser_result["result"]["error"], true) + ##ui_handler._input_requested.emit("") + + +static func _get_ui_handler_instance_from_path(path: String) -> GDShellUIHandler: + var scene: Resource = load(path) + if not scene is PackedScene: + push_error("[GDShell] Attempted to get a GDShellUIHandler instance from '%s' but resource is to a scene." % path) + return null + + var instance: Node = (scene as PackedScene).instantiate() + if not instance is GDShellUIHandler: + push_error("[GDShell] Attempted to get a GDShellUIHandler instance from '%s' but scene root is not of GDShellUIHandler type." % path) + instance.queue_free() + return null + + return instance as GDShellUIHandler static func get_gdshell_version() -> String: @@ -167,8 +215,3 @@ static func get_gdshell_version() -> String: if config.load("res://addons/gdshell/plugin.cfg"): return "Unknown" return str(config.get_value("plugin", "version", "Unknown")) - - -func _input(event: InputEvent) -> void: - if event.is_action(UI_TOGGLE_ACTION) and not event.is_echo() and event.is_pressed(): - ui_handler.toggle_visible() diff --git a/addons/gdshell/scripts/gdshell_main.gd.uid b/addons/gdshell/scripts/gdshell_main.gd.uid new file mode 100644 index 0000000..1525f16 --- /dev/null +++ b/addons/gdshell/scripts/gdshell_main.gd.uid @@ -0,0 +1 @@ +uid://crc2wmm7dhwew diff --git a/addons/gdshell/scripts/gdshell_ui_handler.gd b/addons/gdshell/scripts/gdshell_ui_handler.gd index 5b3d623..4fff6c3 100644 --- a/addons/gdshell/scripts/gdshell_ui_handler.gd +++ b/addons/gdshell/scripts/gdshell_ui_handler.gd @@ -3,40 +3,62 @@ class_name GDShellUIHandler extends Control -signal _input_requested(output: String) -signal _output_requested(output: String, append_new_line: bool) +signal input_submitted(input: String) + + +var _UI_TOGGLE_ACTION: String = str(ProjectSettings.get_setting( + GDShellEditorPlugin.UI_TOGGLE_ACTION, + GDShellEditorPlugin.UI_TOGGLE_ACTION_DEFAULT +)) -var _PARENT_GDSHELL: GDShellMain var history: Array = [] -var hist_index = -1 +var history_index: int = -1 + + +func _input_requested(output: String) -> void: + pass + + +func _output_requested(output: String, append_new_lide: bool = true) -> void: + pass + func submit_input(input: String) -> void: - _PARENT_GDSHELL._submit_input(input) history.push_front(input) history_reset_index() + input_submitted.emit(input) + func autocomplete(input: String) -> String: - var all_commands = _PARENT_GDSHELL.command_db.get_all_command_names() - var matches = all_commands.filter(func(str: String): return str.begins_with(input)) - if matches.size() > 0: - return matches[0] - return input - + #var all_commands = _PARENT_GDSHELL.command_db.get_all_command_names() + #var matches = all_commands.filter( + #func(m: String): + #return m.begins_with(input) + #) + #if matches.size() > 0: + #return matches[0] + #return input + return "" + + func history_get_next() -> String: if (history.size() == 0): return "" - hist_index = clamp(hist_index + 1, 0, history.size() - 1) - return history[hist_index] - + history_index = clamp(history_index + 1, 0, history.size() - 1) + return history[history_index] + + func history_get_previous() -> String: if (history.size() == 0): return "" - hist_index = clamp(hist_index - 1, 0, history.size() - 1) - return history[hist_index] - + history_index = clamp(history_index - 1, 0, history.size() - 1) + return history[history_index] + + func history_reset_index() -> void: - hist_index = -1 + history_index = -1 + func toggle_visible() -> void: visible = not visible diff --git a/addons/gdshell/scripts/gdshell_ui_handler.gd.uid b/addons/gdshell/scripts/gdshell_ui_handler.gd.uid new file mode 100644 index 0000000..765b826 --- /dev/null +++ b/addons/gdshell/scripts/gdshell_ui_handler.gd.uid @@ -0,0 +1 @@ +uid://cbm77ncihcsgl diff --git a/addons/gdshell/ui/default_ui/default_ui.gd b/addons/gdshell/ui/default_ui/default_ui.gd index b4bcafa..d093f66 100644 --- a/addons/gdshell/ui/default_ui/default_ui.gd +++ b/addons/gdshell/ui/default_ui/default_ui.gd @@ -1,6 +1,5 @@ @tool extends GDShellUIHandler -# The default ui extends a PanelContainer instead of a plain Control const DEFAULT_FONT: Font = preload("res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf") const BOLD_FONT: Font = preload("res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf") @@ -9,7 +8,6 @@ const BOLD_ITALICS_FONT: Font = preload("res://addons/gdshell/ui/fonts/roboto_mo # This looks scary, doesn't it? @export_category("GDShell UI") - @export_group("Fonts") @export var default_font: Font = DEFAULT_FONT: set(value): @@ -86,13 +84,18 @@ var _is_input_requested: bool = true: ) -func _ready(): +func _ready() -> void: visibility_changed.connect(_on_visibility_changed) - _input_requested.connect(_handle_input) - _output_requested.connect(_handle_output) + #input_requested.connect(_handle_input) + #output_requested.connect(_handle_output) set_deferred(&"_is_input_requested", true) +func _input(event: InputEvent) -> void: + if event.is_action(_UI_TOGGLE_ACTION) and not event.is_echo() and event.is_pressed(): + toggle_visible() + + func _handle_input(out: String) -> void: set_deferred(&"_is_input_requested", true) _handle_output(out, false) @@ -124,25 +127,27 @@ func _on_visibility_changed() -> void: else: input_line_edit.release_focus() -func set_line_edit_caret_to_end(): +func set_line_edit_caret_to_end() -> void: input_line_edit.grab_focus() input_line_edit.caret_column = input_line_edit.text.length() -func set_line_edit_caret_to_beginning(): +func set_line_edit_caret_to_beginning() -> void: input_line_edit.caret_column = 0 - + + func text_before_caret() -> String: return input_line_edit.text.substr(0, input_line_edit.caret_column) -func _on_input_line_edit_gui_input(event): + +func _on_input_line_edit_gui_input(event: InputEvent) -> void: if (event is InputEventKey and event.pressed): - if event.keycode == KEY_TAB: + if (event as InputEventKey).keycode == KEY_TAB: input_line_edit.text = autocomplete(text_before_caret()) set_line_edit_caret_to_end.call_deferred() - elif event.keycode == KEY_UP: + elif (event as InputEventKey).keycode == KEY_UP: input_line_edit.text = history_get_next() set_line_edit_caret_to_end.call_deferred() - elif event.keycode == KEY_DOWN: + elif (event as InputEventKey).keycode == KEY_DOWN: input_line_edit.text = history_get_previous() set_line_edit_caret_to_end.call_deferred() else: diff --git a/addons/gdshell/ui/default_ui/default_ui.gd.uid b/addons/gdshell/ui/default_ui/default_ui.gd.uid new file mode 100644 index 0000000..a454d0e --- /dev/null +++ b/addons/gdshell/ui/default_ui/default_ui.gd.uid @@ -0,0 +1 @@ +uid://dyo5rm5v27am7 diff --git a/addons/gdshell/ui/default_ui/default_ui.tscn b/addons/gdshell/ui/default_ui/default_ui.tscn index 73a0205..fe0a073 100644 --- a/addons/gdshell/ui/default_ui/default_ui.tscn +++ b/addons/gdshell/ui/default_ui/default_ui.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=14 format=3 uid="uid://dkixrvtb3a1f8"] -[ext_resource type="Script" path="res://addons/gdshell/ui/default_ui/default_ui.gd" id="2_7mabp"] +[ext_resource type="Script" uid="uid://dyo5rm5v27am7" path="res://addons/gdshell/ui/default_ui/default_ui.gd" id="2_7mabp"] [ext_resource type="FontFile" uid="uid://b8fo1gccd11mc" path="res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf" id="2_cto07"] [ext_resource type="FontFile" uid="uid://by5705qqqugvi" path="res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf" id="2_ncxo2"] [ext_resource type="FontFile" uid="uid://6d4ig2mw72j" path="res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf" id="2_xle7x"] diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import index e44ee67..25454c0 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Bold.ttf-4b2e484e86ae1add3920f3eb2 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 @@ -22,6 +23,7 @@ allow_system_fallback=true force_autohinter=false hinting=1 subpixel_positioning=1 +keep_rounding_remainders=true oversampling=0.0 Fallbacks=null fallbacks=[] diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import index 3e62b44..c41a92e 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-BoldItalic.ttf-504bb32bf16b4fd66c1 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 @@ -22,6 +23,7 @@ allow_system_fallback=true force_autohinter=false hinting=1 subpixel_positioning=1 +keep_rounding_remainders=true oversampling=0.0 Fallbacks=null fallbacks=[] diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import index 85f6092..c701d50 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Italic.ttf-badc5e851feaad0fe475224 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 @@ -22,6 +23,7 @@ allow_system_fallback=true force_autohinter=false hinting=1 subpixel_positioning=1 +keep_rounding_remainders=true oversampling=0.0 Fallbacks=null fallbacks=[] diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import index a9160bb..04f6075 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Regular.ttf-fc30ee082ec3388fa60a14 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 @@ -22,6 +23,7 @@ allow_system_fallback=true force_autohinter=false hinting=1 subpixel_positioning=1 +keep_rounding_remainders=true oversampling=0.0 Fallbacks=null fallbacks=[] diff --git a/project.godot b/project.godot index f38f1a4..120b573 100644 --- a/project.godot +++ b/project.godot @@ -11,9 +11,9 @@ config_version=5 [application] config/name="GDShell" -config/description="Feature-packed customizable in-game console for development, debugging, cheats, etc... for Godot 4" +config/description="Light-weight customizable in-game console for development, debugging, cheats, etc... for Godot 4." run/main_scene="res://addons/gdshell/demo/demo.tscn" -config/features=PackedStringArray("4.0", "Forward Plus") +config/features=PackedStringArray("4.4") config/icon="res://addons/gdshell/icon.png" [autoload] @@ -23,20 +23,30 @@ GDShell="*res://addons/gdshell/scripts/gdshell_main.gd" [debug] gdscript/warnings/exclude_addons=false -gdscript/warnings/return_value_discarded=1 +gdscript/warnings/untyped_declaration=1 +gdscript/warnings/inferred_declaration=1 gdscript/warnings/unsafe_property_access=1 gdscript/warnings/unsafe_method_access=1 gdscript/warnings/unsafe_cast=1 gdscript/warnings/unsafe_call_argument=1 +gdscript/warnings/return_value_discarded=1 [editor_plugins] enabled=PackedStringArray("res://addons/gdshell/plugin.cfg") +[filesystem] + +import/blender/enabled=false + [input] gdshell_toggle_ui={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":96,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":96,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } + +[rendering] + +renderer/rendering_method="gl_compatibility"