diff --git a/langtons-ant/.editorconfig b/langtons-ant/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/langtons-ant/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/langtons-ant/.gitattributes b/langtons-ant/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/langtons-ant/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/langtons-ant/.gitignore b/langtons-ant/.gitignore new file mode 100644 index 0000000..dfe3bfb --- /dev/null +++ b/langtons-ant/.gitignore @@ -0,0 +1,4 @@ +# Godot 4+ specific ignores +.godot/ +project.godot +*.import diff --git a/langtons-ant/LICENSE.md b/langtons-ant/LICENSE.md new file mode 100644 index 0000000..179dd21 --- /dev/null +++ b/langtons-ant/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Joshua Najera + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/langtons-ant/README.md b/langtons-ant/README.md new file mode 100644 index 0000000..2eb7d3e --- /dev/null +++ b/langtons-ant/README.md @@ -0,0 +1,55 @@ +VIM bindings for Godot 4 + +recently improved thanks to wenqiangwang +If you would like ctrl+F to be move-forward by page then uncomment the following line + +#"Ctrl+F": 1, ## Uncomment if you want Ctrl+F for move forward by page + +### Supported Mode + + - Normal mode + - Insert mode + - Visual mode + - Visual line mode + +### Supported motions + + h, l, j, k, +, - + ^, 0, $, | + H, L, M, + c-f, c-b, c-d, c-u, + G, gg + w, W, e, E, b, ge + %, f, F, t, T, ; + *, #, /, n, N + aw, a(, a{, a[, a", a' + iw, i(, i{, i[, i", i' + +### Supported operator + + c, C, + d, D, x, X, + y, Y, + u, U, ~ + +### Supported actions + + p, + u, c-r, + c-o, c-i, + za, zM, zR, + q, @, ., + >, < + m, ' + +### Override Default Godot Shortcuts with `godot-vim`'s ones + +Note that all non-ascii character mappings that are already mapped in the default Godot editor have to be unmapped from the Editor settings (Editor >> Editor Settings >> Shorcuts) before being usable with `godot-vim`. + +This currently goes for: + +- `Ctrl+R` +- `Ctrl+U` +- `Ctrl+D` + +See the full list of non-ascii shortucts that may already be mapped by Godot and thus wouldn't work in `godot-vim` before releasing them in Godot settings: https://github.com/joshnajera/godot-vim/blob/main/addons/godot-vim/godot-vim.gd#L135 diff --git a/langtons-ant/TODO b/langtons-ant/TODO new file mode 100644 index 0000000..ecfb606 --- /dev/null +++ b/langtons-ant/TODO @@ -0,0 +1,2 @@ +Ant navigation is wonky -> check if next_dir behaves as expected. +ant sprite does not budge from centre of screen no matter what i try -> fix that diff --git a/langtons-ant/addons/godot-vim/godot-vim.gd b/langtons-ant/addons/godot-vim/godot-vim.gd new file mode 100644 index 0000000..be6183c --- /dev/null +++ b/langtons-ant/addons/godot-vim/godot-vim.gd @@ -0,0 +1,1685 @@ +@tool +extends EditorPlugin + +const INF_COL : int = 99999 +const DEBUGGING : int = 0 # Change to 1 for debugging +const CODE_MACRO_PLAY_END : int = 10000 + +const BREAKERS : Dictionary = { '!': 1, '"': 1, '#': 1, '$': 1, '%': 1, '&': 1, '(': 1, ')': 1, '*': 1, '+': 1, ',': 1, '-': 1, '.': 1, '/': 1, ':': 1, ';': 1, '<': 1, '=': 1, '>': 1, '?': 1, '@': 1, '[': 1, '\\': 1, ']': 1, '^': 1, '`': 1, '\'': 1, '{': 1, '|': 1, '}': 1, '~': 1 } +const WHITESPACE: Dictionary = { ' ': 1, ' ': 1, '\n' : 1 } +const ALPHANUMERIC: Dictionary = { 'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1, 'f': 1, 'g': 1, 'h': 1, 'i': 1, 'j': 1, 'k': 1, 'l': 1, 'm': 1, 'n': 1, 'o': 1, 'p': 1, 'q': 1, 'r': 1, 's': 1, 't': 1, 'u': 1, 'v': 1, 'w': 1, 'x': 1, 'y': 1, 'z': 1, 'A': 1, 'B': 1, 'C': 1, 'D': 1, 'E': 1, 'F': 1, 'G': 1, 'H': 1, 'I': 1, 'J': 1, 'K': 1, 'L': 1, 'M': 1, 'N': 1, 'O': 1, 'P': 1, 'Q': 1, 'R': 1, 'S': 1, 'T': 1, 'U': 1, 'V': 1, 'W': 1, 'X': 1, 'Y': 1, 'Z': 1, '0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '6': 1, '7': 1, '8': 1, '9': 1, '_': 1 } +const LOWER_ALPHA: Dictionary = { 'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1, 'f': 1, 'g': 1, 'h': 1, 'i': 1, 'j': 1, 'k': 1, 'l': 1, 'm': 1, 'n': 1, 'o': 1, 'p': 1, 'q': 1, 'r': 1, 's': 1, 't': 1, 'u': 1, 'v': 1, 'w': 1, 'x': 1, 'y': 1, 'z': 1 } +const SYMBOLS = { "(": ")", ")": "(", "[": "]", "]": "[", "{": "}", "}": "{", "<": ">", ">": "<", '"': '"', "'": "'" } + + +enum { + MOTION, + OPERATOR, + OPERATOR_MOTION, + ACTION, +} + + +enum Context { + NORMAL, + VISUAL, +} + + +var the_key_map : Array[Dictionary] = [ + # Move + { "keys": ["H"], "type": MOTION, "motion": "move_by_characters", "motion_args": { "forward": false } }, + { "keys": ["L"], "type": MOTION, "motion": "move_by_characters", "motion_args": { "forward": true } }, + { "keys": ["J"], "type": MOTION, "motion": "move_by_lines", "motion_args": { "forward": true, "line_wise": true } }, + { "keys": ["K"], "type": MOTION, "motion": "move_by_lines", "motion_args": { "forward": false, "line_wise": true } }, + { "keys": ["Shift+Equal"], "type": MOTION, "motion": "move_by_lines", "motion_args": { "forward": true, "to_first_char": true } }, + { "keys": ["Minus"], "type": MOTION, "motion": "move_by_lines", "motion_args": { "forward": false, "to_first_char": true } }, + { "keys": ["Shift+4"], "type": MOTION, "motion": "move_to_end_of_line", "motion_args": { "inclusive": true } }, + { "keys": ["Shift+6"], "type": MOTION, "motion": "move_to_first_non_white_space_character", "motion_args": { "change_line": false } }, + { "keys": ["Shift+Minus"], "type": MOTION, "motion": "move_to_first_non_white_space_character", "motion_args": { "change_line": true } }, + { "keys": ["0"], "type": MOTION, "motion": "move_to_start_of_line" }, + { "keys": ["Shift+H"], "type": MOTION, "motion": "move_to_top_line", "motion_args": { "to_jump_list": true } }, + { "keys": ["Shift+L"], "type": MOTION, "motion": "move_to_bottom_line", "motion_args": { "to_jump_list": true } }, + { "keys": ["Shift+M"], "type": MOTION, "motion": "move_to_middle_line", "motion_args": { "to_jump_list": true } }, + { "keys": ["G", "G"], "type": MOTION, "motion": "move_to_line_or_edge_of_document", "motion_args": { "forward": false, "to_jump_list": true } }, + { "keys": ["Shift+G"], "type": MOTION, "motion": "move_to_line_or_edge_of_document", "motion_args": { "forward": true, "to_jump_list": true } }, + { "keys": ["Ctrl+F"], "type": MOTION, "motion": "move_by_page", "motion_args": { "forward": true } }, + { "keys": ["Ctrl+B"], "type": MOTION, "motion": "move_by_page", "motion_args": { "forward": false } }, + { "keys": ["Ctrl+D"], "type": MOTION, "motion": "move_by_scroll", "motion_args": { "forward": true } }, + { "keys": ["Ctrl+U"], "type": MOTION, "motion": "move_by_scroll", "motion_args": { "forward": false } }, + { "keys": ["Shift+BackSlash"], "type": MOTION, "motion": "move_to_column" }, + { "keys": ["W"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": true, "word_end": false, "big_word": false } }, + { "keys": ["Shift+W"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": true, "word_end": false, "big_word": true } }, + { "keys": ["E"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": true, "word_end": true, "big_word": false, "inclusive": true } }, + { "keys": ["Shift+E"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": true, "word_end": true, "big_word": true, "inclusive": true } }, + { "keys": ["B"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": false, "word_end": false, "big_word": false } }, + { "keys": ["Shift+B"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": false, "word_end": false, "big_word": true } }, + { "keys": ["G", "E"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": false, "word_end": true, "big_word": false } }, + { "keys": ["G", "Shift+E"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": false, "word_end": true, "big_word": true } }, + { "keys": ["Shift+5"], "type": MOTION, "motion": "move_to_matched_symbol", "motion_args": { "inclusive": true, "to_jump_list": true } }, + { "keys": ["F", "{char}"], "type": MOTION, "motion": "move_to_next_char", "motion_args": { "forward": true, "inclusive": true } }, + { "keys": ["Shift+F", "{char}"], "type": MOTION, "motion": "move_to_next_char", "motion_args": { "forward": false } }, + { "keys": ["T", "{char}"], "type": MOTION, "motion": "move_to_next_char", "motion_args": { "forward": true, "stop_before": true, "inclusive": true } }, + { "keys": ["Shift+T", "{char}"], "type": MOTION, "motion": "move_to_next_char", "motion_args": { "forward": false, "stop_before": true } }, + { "keys": ["Semicolon"], "type": MOTION, "motion": "repeat_last_char_search", "motion_args": {} }, + { "keys": ["Shift+8"], "type": MOTION, "motion": "find_word_under_caret", "motion_args": { "forward": true, "to_jump_list": true } }, + { "keys": ["Shift+3"], "type": MOTION, "motion": "find_word_under_caret", "motion_args": { "forward": false, "to_jump_list": true } }, + { "keys": ["N"], "type": MOTION, "motion": "find_again", "motion_args": { "forward": true, "to_jump_list": true } }, + { "keys": ["Shift+N"], "type": MOTION, "motion": "find_again", "motion_args": { "forward": false, "to_jump_list": true } }, + { "keys": ["A", "Shift+9"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"(" } }, + { "keys": ["A", "Shift+0"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"(" } }, + { "keys": ["A", "B"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"(" } }, + { "keys": ["A", "BracketLeft"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"[" } }, + { "keys": ["A", "BracketRight"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"[" } }, + { "keys": ["A", "Shift+BracketLeft"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"{" } }, + { "keys": ["A", "Shift+BracketRight"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"{" } }, + { "keys": ["A", "Shift+B"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"{" } }, + { "keys": ["A", "Apostrophe"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"'" } }, + { "keys": ["A", 'Shift+Apostrophe'], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":'"' } }, + { "keys": ["I", "Shift+9"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"(" } }, + { "keys": ["I", "Shift+0"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"(" } }, + { "keys": ["I", "B"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"(" } }, + { "keys": ["I", "BracketLeft"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"[" } }, + { "keys": ["I", "BracketRight"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"[" } }, + { "keys": ["I", "Shift+BracketLeft"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"{" } }, + { "keys": ["I", "Shift+BracketRight"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"{" } }, + { "keys": ["I", "Shift+B"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"{" } }, + { "keys": ["I", "Apostrophe"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"'" } }, + { "keys": ["I", 'Shift+Apostrophe'], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":'"' } }, + { "keys": ["I", "W"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"w" } }, + { "keys": ["D"], "type": OPERATOR, "operator": "delete" }, + { "keys": ["Shift+D"], "type": OPERATOR_MOTION, "operator": "delete", "motion": "move_to_end_of_line", "motion_args": { "inclusive": true } }, + { "keys": ["Y"], "type": OPERATOR, "operator": "yank" }, + { "keys": ["Shift+Y"], "type": OPERATOR_MOTION, "operator": "yank", "motion": "move_to_end_of_line", "motion_args": { "inclusive": true } }, + { "keys": ["C"], "type": OPERATOR, "operator": "change", "operator_args": {"normal_mode_after_visual": false}}, + { "keys": ["Shift+C"], "type": OPERATOR_MOTION, "operator": "change", "motion": "move_to_end_of_line", "motion_args": { "inclusive": true } }, + { "keys": ["X"], "type": OPERATOR_MOTION, "operator": "delete", "motion": "move_by_characters", "motion_args": { "forward": true, "one_line": true }, "context": Context.NORMAL }, + { "keys": ["S"], "type": OPERATOR_MOTION, "operator": "delete_and_enter_insert_mode", "motion": "move_by_characters", "motion_args": { "forward": true }, "context": Context.NORMAL }, + { "keys": ["X"], "type": OPERATOR, "operator": "delete", "context": Context.VISUAL }, + { "keys": ["Shift+X"], "type": OPERATOR_MOTION, "operator": "delete", "motion": "move_by_characters", "motion_args": { "forward": false } }, + { "keys": ["U"], "type": OPERATOR, "operator": "change_case", "operator_args": { "lower": true }, "context": Context.VISUAL }, + { "keys": ["Shift+U"], "type": OPERATOR, "operator": "change_case", "operator_args": { "lower": false }, "context": Context.VISUAL }, + { "keys": ["Shift+QuoteLeft"], "type": OPERATOR, "operator": "toggle_case", "operator_args": {}, "context": Context.VISUAL }, + { "keys": ["Shift+QuoteLeft"], "type": OPERATOR_MOTION, "operator": "toggle_case", "motion": "move_by_characters", "motion_args": { "forward": true }, "context": Context.NORMAL }, + { "keys": ["P"], "type": ACTION, "action": "paste", "action_args": { "after": true } }, + { "keys": ["Shift+P"], "type": ACTION, "action": "paste", "action_args": { "after": false } }, + { "keys": ["U"], "type": ACTION, "action": "undo", "action_args": {}, "context": Context.NORMAL }, + { "keys": ["Ctrl+R"], "type": ACTION, "action": "redo", "action_args": {} }, + { "keys": ["R", "{char}"], "type": ACTION, "action": "replace", "action_args": {} }, + { "keys": ["Period"], "type": ACTION, "action": "repeat_last_edit", "action_args": {} }, + { "keys": ["I"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "inplace" }, "context": Context.NORMAL }, + { "keys": ["Shift+I"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "bol" } }, + { "keys": ["A"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "after" }, "context": Context.NORMAL }, + { "keys": ["Shift+A"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "eol" } }, + { "keys": ["O"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "new_line_below" } }, + { "keys": ["Shift+O"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "new_line_above" } }, + { "keys": ["V"], "type": ACTION, "action": "enter_visual_mode", "action_args": { "line_wise": false } }, + { "keys": ["Shift+V"], "type": ACTION, "action": "enter_visual_mode", "action_args": { "line_wise": true } }, + { "keys": ["Slash"], "type": ACTION, "action": "search", "action_args": {} }, + { "keys": ["Ctrl+O"], "type": ACTION, "action": "jump_list_walk", "action_args": { "forward": false } }, + { "keys": ["Ctrl+I"], "type": ACTION, "action": "jump_list_walk", "action_args": { "forward": true } }, + { "keys": ["Z", "A"], "type": ACTION, "action": "toggle_folding", }, + { "keys": ["Z", "Shift+M"], "type": ACTION, "action": "fold_all", }, + { "keys": ["Z", "Shift+R"], "type": ACTION, "action": "unfold_all", }, + { "keys": ["Q", "{char}"], "type": ACTION, "action": "record_macro", "when_not": "is_recording" }, + { "keys": ["Q"], "type": ACTION, "action": "stop_record_macro", "when": "is_recording" }, + { "keys": ["Shift+2", "{char}"], "type": ACTION, "action": "play_macro", }, + { "keys": ["Shift+Comma"], "type": ACTION, "action": "indent", "action_args": { "forward" = false } }, + { "keys": ["Shift+Period"], "type": ACTION, "action": "indent", "action_args": { "forward" = true } }, + { "keys": ["Shift+J"], "type": ACTION, "action": "join_lines", "action_args": {} }, + { "keys": ["M", "{char}"], "type": ACTION, "action": "set_bookmark", "action_args": {} }, + { "keys": ["Apostrophe", "{char}"], "type": MOTION, "motion": "go_to_bookmark", "motion_args": {} }, + { "keys": ["Shift+K"], "type": ACTION, "action": "go_to_doc", "context": Context.NORMAL}, + +] + + +# The list of command keys we handle (other command keys will be handled by Godot) +var command_keys_white_list : Dictionary = { + "Escape": 1, + "Enter": 1, + # "Ctrl+F": 1, # Uncomment if you would like move-forward by page function instead of search on slash + "Ctrl+B": 1, + "Ctrl+U": 1, + "Ctrl+D": 1, + #"Ctrl+O": 1, # Prefer to use as Alt+arrow aliases + #"Ctrl+I": 1, + "Ctrl+R": 1 +} + + +var editor_interface : EditorInterface +var the_vim := Vim.new() +var the_ed := EditorAdaptor.new(the_vim) # The current editor adaptor +var the_dispatcher := CommandDispatcher.new(the_key_map) # The command dispatcher + + +func _enter_tree() -> void: + editor_interface = get_editor_interface() + + var script_editor = editor_interface.get_script_editor() + script_editor.editor_script_changed.connect(on_script_changed) + script_editor.script_close.connect(on_script_closed) + on_script_changed(script_editor.get_current_script()) + + var settings = editor_interface.get_editor_settings() + settings.settings_changed.connect(on_settings_changed) + on_settings_changed() + + var find_bar = find_first_node_of_type(script_editor, 'FindReplaceBar') + var find_bar_line_edit : LineEdit = find_first_node_of_type(find_bar, 'LineEdit') + find_bar_line_edit.text_changed.connect(on_search_text_changed) + + +func _input(event) -> void: + var key = event as InputEventKey + + # Don't process when not a key action + if key == null or !key.is_pressed() or not is_instance_valid(the_ed) or not the_ed.has_focus(): + return + + if key.get_keycode_with_modifiers() == KEY_NONE and key.unicode == CODE_MACRO_PLAY_END: + the_vim.macro_manager.on_macro_finished(the_ed) + get_viewport().set_input_as_handled() + return + + # Check to not block some reserved keys (we only handle unicode keys and the white list) + var key_code = key.as_text_keycode() + if DEBUGGING: + print("Key: %s Buffer: %s" % [key_code, the_vim.current.input_state.key_codes()]) + + # We only process keys in the white list or it is ASCII char or SHIFT+ASCII char + if key.get_keycode_with_modifiers() & (~KEY_MASK_SHIFT) > 128 and key_code not in command_keys_white_list: + return + + if the_dispatcher.dispatch(key, the_vim, the_ed): + get_viewport().set_input_as_handled() + + +func on_script_changed(s: Script) -> void: + the_vim.set_current_session(s, the_ed) + + var script_editor = editor_interface.get_script_editor() + var scrpit_editor_base := script_editor.get_current_editor() + if scrpit_editor_base: + var code_editor := scrpit_editor_base.get_base_editor() as CodeEdit + the_ed.set_code_editor(code_editor) + the_ed.set_block_caret(true) + the_ed.set_caret_blink(false) + + if not code_editor.is_connected("caret_changed", on_caret_changed): + code_editor.caret_changed.connect(on_caret_changed) + if not code_editor.is_connected("lines_edited_from", on_lines_edited_from): + code_editor.lines_edited_from.connect(on_lines_edited_from) + + +func on_script_closed(s: Script) -> void: + the_vim.remove_session(s) + + +func on_settings_changed() -> void: + var settings := editor_interface.get_editor_settings() + the_ed.notify_settings_changed(settings) + + +func on_caret_changed()-> void: + the_ed.set_block_caret(not the_vim.current.insert_mode) + + +func on_lines_edited_from(from: int, to: int) -> void: + the_vim.current.jump_list.on_lines_edited(from, to) + the_vim.current.text_change_number += 1 + the_vim.current.bookmark_manager.on_lines_edited(from, to) + + +func on_search_text_changed(new_search_text: String) -> void: + the_vim.search_buffer = new_search_text + + +static func find_first_node_of_type(p: Node, type: String) -> Node: + if p.get_class() == type: + return p + for c in p.get_children(): + var t := find_first_node_of_type(c, type) + if t: + return t + return null + + +class Command: + + ### MOTIONS + + static func move_by_characters(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var one_line = args.get('one_line', false) + var col : int = cur.column + args.repeat * (1 if args.forward else -1) + var line := cur.line + if col > ed.last_column(line): + if one_line: + col = ed.last_column(line) + 1 + else: + line += 1 + col = 0 + elif col < 0: + if one_line: + col = 0 + else: + line -= 1 + col = ed.last_column(line) + # if vim.current.visual_mode: + # col -= 1 + return Position.new(line, col) + + static func move_by_scroll(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var count = ed.get_visible_line_count(ed.first_visible_line(), ed.last_visible_line()) + return Position.new(ed.next_unfolded_line(cur.line, count / 2, args.forward), cur.column) + + static func move_by_page(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var count = ed.get_visible_line_count(ed.first_visible_line(), ed.last_visible_line()) + return Position.new(ed.next_unfolded_line(cur.line, count, args.forward), cur.column) + + static func move_to_column(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + return Position.new(cur.line, args.repeat - 1) + + static func move_by_lines(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + # Depending what our last motion was, we may want to do different things. + # If our last motion was moving vertically, we want to preserve the column from our + # last horizontal move. If our last motion was going to the end of a line, + # moving vertically we should go to the end of the line, etc. + var col : int = cur.column + match vim.current.last_motion: + "move_by_lines", "move_by_scroll", "move_by_page", "move_to_end_of_line", "move_to_column": + col = vim.current.last_h_pos + _: + vim.current.last_h_pos = col + + var line = ed.next_unfolded_line(cur.line, args.repeat, args.forward) + + if args.get("to_first_char", false): + col = ed.find_first_non_white_space_character(line) + + return Position.new(line, col) + + static func move_to_first_non_white_space_character(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var new_line = cur.line + if args.change_line: + new_line += args.repeat - 1 + var i := ed.find_first_non_white_space_character(new_line) + return Position.new(new_line, i) + + static func move_to_start_of_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + return Position.new(cur.line, 0) + + static func move_to_end_of_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var line = cur.line + if args.repeat > 1: + line = ed.next_unfolded_line(line, args.repeat - 1) + vim.current.last_h_pos = INF_COL + return Position.new(line, ed.last_column(line)) + + static func move_to_top_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + return Position.new(ed.first_visible_line(), cur.column) + + static func move_to_bottom_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + return Position.new(ed.last_visible_line(), cur.column) + + static func move_to_middle_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var first := ed.first_visible_line() + var count = ed.get_visible_line_count(first, ed.last_visible_line()) + return Position.new(ed.next_unfolded_line(first, count / 2), cur.column) + + static func move_to_line_or_edge_of_document(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var line = ed.last_line() if args.forward else ed.first_line() + if args.repeat_is_explicit: + line = args.repeat + ed.first_line() - 1 + return Position.new(line, ed.find_first_non_white_space_character(line)) + + static func move_by_words(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var start_line := cur.line + var start_col := cur.column + var start_pos := cur.duplicate() + + # If we are beyond line end, move it to line end + if start_col > 0 and start_col == ed.last_column(start_line) + 1: + cur = Position.new(start_line, start_col-1) + + var forward : bool = args.forward + var word_end : bool = args.word_end + var big_word : bool = args.big_word + var repeat : int = args.repeat + var empty_line_is_word := not (forward and word_end) # For 'e', empty lines are not considered words + var one_line := not vim.current.input_state.operator.is_empty() # if there is an operator pending, let it not beyond the line end each time + + if (forward and !word_end) or (not forward and word_end): # w or ge + repeat += 1 + + var words : Array[TextRange] = [] + for i in range(repeat): + var word = _find_word(cur, ed, forward, big_word, empty_line_is_word, one_line) + if word != null: + words.append(word) + cur = Position.new(word.from.line, word.to.column-1 if forward else word.from.column) + else: # eof + words.append(TextRange.new(ed.last_pos(), ed.last_pos()) if forward else TextRange.zero) + break + + var short_circuit : bool = len(words) != repeat + var first_word := words[0] + var last_word : TextRange = words.pop_back() + if forward and not word_end: # w + if vim.current.input_state.operator == "change": # cw need special treatment to not cover whitespaces + if not short_circuit: + last_word = words.pop_back() + return last_word.to + if not short_circuit and not start_pos.equals(first_word.from): + last_word = words.pop_back() # We did not start in the middle of a word. Discard the extra word at the end. + return last_word.from + elif forward and word_end: # e + return last_word.to.left() + elif not forward and word_end: # ge + if not short_circuit and not start_pos.equals(first_word.to.left()): + last_word = words.pop_back() # We did not start in the middle of a word. Discard the extra word at the end. + return last_word.to.left() + else: # b + return last_word.from + + static func move_to_matched_symbol(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + # Get the symbol to match + var symbol := ed.find_forward(cur.line, cur.column, func(c): return c.char in "()[]{}", true) + if symbol == null: # No symbol found in this line after or under caret + return null + + var counter_part : String = SYMBOLS[symbol.char] + + # Two attemps to find the symbol pair: from line start or doc start + for from in [Position.new(symbol.line, 0), Position.new(0, 0)]: + var parser = GDScriptParser.new(ed, from) + if not parser.parse_until(symbol): + continue + + if symbol.char in ")]}": + parser.stack.reverse() + for p in parser.stack: + if p.char == counter_part: + return p + continue + else: + parser.parse_one_char() + return parser.find_matching() + return null + + static func move_to_next_char(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + vim.last_char_search = args + + var forward : bool = args.forward + var stop_before : bool = args.get("stop_before", false) + var to_find = args.selected_character + var repeat : int = args.repeat + + var old_pos := cur.duplicate() + for ch in ed.chars(cur.line, cur.column + (1 if forward else -1), forward, true): + if ch.char == to_find: + repeat -= 1 + if repeat == 0: + return old_pos if stop_before else Position.new(ch.line, ch.column) + old_pos = Position.new(ch.line, ch.column) + return null + + static func repeat_last_char_search(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var last_char_search := vim.last_char_search + if last_char_search.is_empty(): + return null + args.forward = last_char_search.forward + args.selected_character = last_char_search.selected_character + args.stop_before = last_char_search.get("stop_before", false) + args.inclusive = last_char_search.get("inclusive", false) + return move_to_next_char(cur, args, ed, vim) + + static func expand_to_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + return Position.new(cur.line + args.repeat - 1, INF_COL) + + static func find_word_under_caret(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var forward : bool = args.forward + var range := ed.get_word_at_pos(cur.line, cur.column) + var text := ed.range_text(range) + var pos := ed.search(text, cur.line, cur.column + (1 if forward else -1), false, true, forward) + vim.last_search_command = "*" if forward else "#" + vim.search_buffer = text + return pos + + static func find_again(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var forward : bool = args.forward + forward = forward == (vim.last_search_command != "#") + var case_sensitive := vim.last_search_command in "*#" + var whole_word := vim.last_search_command in "*#" + cur = cur.next(ed) if forward else cur.prev(ed) + return ed.search(vim.search_buffer, cur.line, cur.column, case_sensitive, whole_word, forward) + + static func text_object(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Variant: + var inner : bool = args.inner + var obj : String = args.object + + if obj == "w" and inner: + return ed.get_word_at_pos(cur.line, cur.column) + + if obj in "([{\"'": + var counter_part : String = SYMBOLS[obj] + for from in [Position.new(cur.line, 0), Position.new(0, 0)]: # Two attemps: from line beginning doc beginning + var parser = GDScriptParser.new(ed, from) + if not parser.parse_until(cur): + continue + + var range = TextRange.zero + if parser.stack_top_char == obj: + range.from = parser.stack.back() + range.to = parser.find_matching() + elif ed.char_at(cur.line, cur.column) == obj: + parser.parse_one_char() + range.from = parser.pos + range.to = parser.find_matching() + else: + continue + + if range.from == null or range.to == null: + continue + + if inner: + range.from = range.from.next(ed) + else: + range.to = range.to.next(ed) + return range + + return null + + +### OPERATORS + + static func delete(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var text := ed.selected_text() + vim.register.set_text(text, args.get("line_wise", false)) + ed.delete_selection() + var line := ed.curr_line() + var col := ed.curr_column() + if col > ed.last_column(line): # If after deletion we are beyond the end, move left + ed.set_curr_column(ed.last_column(line)) + + static func yank(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var text := ed.selected_text() + ed.deselect() + vim.register.set_text(text, args.get("line_wise", false)) + + static func change(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var text := ed.selected_text() + vim.register.set_text(text, args.get("line_wise", false)) + + ed.delete_selection() + vim.current.enter_insert_mode() + + static func change_case(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var lower_case : bool = args.get("lower", false) + var text := ed.selected_text() + ed.replace_selection(text.to_lower() if lower_case else text.to_upper()) + + static func toggle_case(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var text := ed.selected_text() + var s := PackedStringArray() + for c in text: + s.append(c.to_lower() if c == c.to_upper() else c.to_upper()) + ed.replace_selection(''.join(s)) + + static func delete_and_enter_insert_mode(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var text := ed.selected_text() + vim.register.set_text(text, args.get("line_wise", false)) + ed.delete_selection() + var line := ed.curr_line() + var col := ed.curr_column() + if col > ed.last_column(line): # If after deletion we are beyond the end, move left + ed.set_curr_column(ed.last_column(line)) + vim.current.enter_insert_mode(); + + ### ACTIONS + + static func paste(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var after : bool = args.after + var line_wise := vim.register.line_wise + var clipboard_text := vim.register.text + + var text : String = "" + for i in range(args.repeat): + text += clipboard_text + + var line := ed.curr_line() + var col := ed.curr_column() + + if line_wise: + if after: + text = "\n" + text.substr(0, len(text)-1) + col = len(ed.line_text(line)) + else: + col = 0 + else: + col += 1 if after else 0 + + ed.set_curr_column(col) + ed.insert_text(text) + + static func undo(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + for i in range(args.repeat): + ed.undo() + ed.deselect() + + static func redo(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + for i in range(args.repeat): + ed.redo() + ed.deselect() + + static func replace(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var to_replace = args.selected_character + var line := ed.curr_line() + var col := ed.curr_column() + ed.select(line, col, line, col+1) + ed.replace_selection(to_replace) + + static func enter_insert_mode(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var insert_at : String = args.insert_at + var line = ed.curr_line() + var col = ed.curr_column() + + vim.current.enter_insert_mode() + + match insert_at: + "inplace": + pass + "after": + ed.set_curr_column(col + 1) + "bol": + ed.set_curr_column(ed.find_first_non_white_space_character(line)) + "eol": + ed.set_curr_column(INF_COL) + "new_line_below": + ed.set_curr_column(INF_COL) + ed.simulate_press(KEY_ENTER) + "new_line_above": + ed.set_curr_column(0) + if line == ed.first_line(): + ed.insert_text("\n") + ed.jump_to(0, 0) + else: + ed.jump_to(line - 1, INF_COL) + ed.simulate_press(KEY_ENTER) + + static func enter_visual_mode(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var line_wise : bool = args.get('line_wise', false) + vim.current.enter_visual_mode(line_wise) + + static func search(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + if OS.get_name() == "macOS": + ed.simulate_press(KEY_F, 0, false, false, false, true) + else: + ed.simulate_press(KEY_F, 0, true, false, false, false) + vim.last_search_command = "/" + + static func jump_list_walk(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var offset : int = args.repeat * (1 if args.forward else -1) + var pos : Position = vim.current.jump_list.move(offset) + if pos != null: + if not args.forward: + vim.current.jump_list.set_next(ed.curr_position()) + ed.jump_to(pos.line, pos.column) + + static func toggle_folding(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + ed.toggle_folding(ed.curr_line()) + + static func fold_all(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + ed.fold_all() + + static func unfold_all(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + ed.unfold_all() + + static func repeat_last_edit(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var repeat : int = args.repeat + vim.macro_manager.play_macro(repeat, ".", ed) + + static func record_macro(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var name = args.selected_character + if name in ALPHANUMERIC: + vim.macro_manager.start_record_macro(name) + + static func stop_record_macro(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + vim.macro_manager.stop_record_macro() + + static func play_macro(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var name = args.selected_character + var repeat : int = args.repeat + if name in ALPHANUMERIC: + vim.macro_manager.play_macro(repeat, name, ed) + + static func is_recording(ed: EditorAdaptor, vim: Vim) -> bool: + return vim.macro_manager.is_recording() + + static func indent(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var repeat : int = args.repeat + var forward : bool = args.get("forward", false) + var range = ed.selection() + + if not range.is_single_line() and range.to.column == 0: # Don't select the last empty line + ed.select(range.from.line, range.from.column, range.to.line-1, INF_COL) + + ed.begin_complex_operation() + for i in range(repeat): + if forward: + ed.indent() + else: + ed.unindent() + ed.end_complex_operation() + + static func join_lines(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + if vim.current.normal_mode: + var line := ed.curr_line() + ed.select(line, 0, line + args.repeat, INF_COL) + + var range := ed.selection() + ed.select(range.from.line, 0, range.to.line, INF_COL) + var s := PackedStringArray() + s.append(ed.line_text(range.from.line)) + for line in range(range.from.line + 1, range.to.line + 1): + s.append(ed.line_text(line).lstrip(' \t\n')) + ed.replace_selection(' '.join(s)) + + static func set_bookmark(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var name = args.selected_character + if name in LOWER_ALPHA: + vim.current.bookmark_manager.set_bookmark(name, ed.curr_line()) + + static func go_to_bookmark(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: + var name = args.selected_character + var line := vim.current.bookmark_manager.get_bookmark(name) + if line < 0: + return null + return Position.new(line, 0) + + static func go_to_doc(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + var text: String = ed.line_text(ed.curr_line()) + var begin = text.rfind("res://", ed.curr_column()) + var symbol = text[begin-1] + var end = text.find(symbol, begin) + var search_word: String + if ed.curr_column() <= end: + search_word = text.substr(begin, end - begin) + else: + search_word = ed.code_editor.get_word_under_caret() + ed.code_editor.symbol_lookup.emit(search_word, ed.curr_line(), ed.curr_column()) + + + ### HELPER FUNCTIONS + + ## Returns the boundaries of the next word. If the cursor in the middle of the word, then returns the boundaries of the current word, starting at the cursor. + ## If the cursor is at the start/end of a word, and we are going forward/backward, respectively, find the boundaries of the next word. + static func _find_word(cur: Position, ed: EditorAdaptor, forward: bool, big_word: bool, empty_line_is_word: bool, one_line: bool) -> TextRange: + var char_tests := [ func(c): return c in ALPHANUMERIC or c in BREAKERS ] if big_word else [ func(c): return c in ALPHANUMERIC, func(c): return c in BREAKERS ] + + for p in ed.chars(cur.line, cur.column, forward): + if one_line and p.char == '\n': # If we only allow search in one line and we met the line end + return TextRange.from_num3(p.line, p.column, INF_COL) + + if p.line != cur.line and empty_line_is_word and p.line_text.strip_edges() == '': + return TextRange.from_num3(p.line, 0, 0) + + for char_test in char_tests: + if char_test.call(p.char): + var word_start := p.column + var word_end := word_start + for q in ed.chars(p.line, p.column, forward, true): # Advance to end of word. + if not char_test.call(q.char): + break + word_end = q.column + + if p.line == cur.line and word_start == cur.column and word_end == word_start: + continue # We started at the end of a word. Find the next one. + else: + return TextRange.from_num3(p.line, min(word_start, word_end), max(word_start + 1, word_end + 1)) + return null + + +class Position: + var line: int + var column: int + + static var zero :Position: + get: + return Position.new(0, 0) + + func _init(l: int, c: int): + line = l + column = c + + func _to_string() -> String: + return "(%s, %s)" % [line, column] + + func equals(other: Position) -> bool: + return line == other.line and column == other.column + + func compares_to(other: Position) -> int: + if line < other.line: return -1 + if line > other.line: return 1 + if column < other.column: return -1 + if column > other.column: return 1 + return 0 + + func duplicate() -> Position: return Position.new(line, column) + func up() -> Position: return Position.new(line-1, column) + func down() -> Position: return Position.new(line+1, column) + func left() -> Position: return Position.new(line, column-1) + func right() -> Position: return Position.new(line, column+1) + func next(ed: EditorAdaptor) -> Position: return ed.offset_pos(self, 1) + func prev(ed: EditorAdaptor) -> Position: return ed.offset_pos(self, -1) + + +class TextRange: + var from: Position + var to: Position + + static var zero : TextRange: + get: + return TextRange.new(Position.zero, Position.zero) + + static func from_num4(from_line: int, from_column: int, to_line: int, to_column: int): + return TextRange.new(Position.new(from_line, from_column), Position.new(to_line, to_column)) + + static func from_num3(line: int, from_column: int, to_column: int): + return from_num4(line, from_column, line, to_column) + + func _init(f: Position, t: Position): + from = f + to = t + + func _to_string() -> String: + return "[%s - %s]" % [from, to] + + func is_single_line() -> bool: + return from.line == to.line + + func is_empty() -> bool: + return from.line == to.line and from.column == to.column + + +class CharPos extends Position: + var line_text : String + + var char: String: + get: + return line_text[column] if column < len(line_text) else '\n' + + func _init(line_text: String, line: int, col: int): + super(line, col) + self.line_text = line_text + + +class JumpList: + var buffer: Array[Position] + var pointer: int = 0 + + func _init(capacity: int = 20): + buffer = [] + buffer.resize(capacity) + + func add(old_pos: Position, new_pos: Position) -> void: + var current : Position = buffer[pointer] + if current == null or not current.equals(old_pos): + pointer = (pointer + 1) % len(buffer) + buffer[pointer] = old_pos + pointer = (pointer + 1) % len(buffer) + buffer[pointer] = new_pos + + func set_next(pos: Position) -> void: + buffer[(pointer + 1) % len(buffer)] = pos # This overrides next forward position (TODO: an insert might be better) + + func move(offset: int) -> Position: + var t := (pointer + offset) % len(buffer) + var r : Position = buffer[t] + if r != null: + pointer = t + return r + + func on_lines_edited(from: int, to: int) -> void: + for pos in buffer: + if pos != null and pos.line > from: # Unfortunately we don't know which column changed + pos.line += to - from + + +class InputState: + var prefix_repeat: String + var motion_repeat: String + var operator: String + var operator_args: Dictionary + var buffer: Array[InputEventKey] = [] + + func push_key(key: InputEventKey) -> void: + buffer.append(key) + + func push_repeat_digit(d: String) -> void: + if operator.is_empty(): + prefix_repeat += d + else: + motion_repeat += d + + func get_repeat() -> int: + var repeat : int = 0 + if prefix_repeat: + repeat = max(repeat, 1) * int(prefix_repeat) + if motion_repeat: + repeat = max(repeat, 1) * int(motion_repeat) + return repeat + + func key_codes() -> Array[String]: + var r : Array[String] = [] + for k in buffer: + r.append(k.as_text_keycode()) + return r + + func clear() -> void: + prefix_repeat = "" + motion_repeat = "" + operator = "" + buffer.clear() + + +class GDScriptParser: + const open_symbol := { "(": ")", "[": "]", "{": "}", "'": "'", '"': '"' } + const close_symbol := { ")": "(", "]": "[", "}": "{", } + + var stack : Array[CharPos] + var in_comment := false + var escape_count := 0 + var valid: bool = true + var eof : bool = false + var pos: Position + + var stack_top_char : String: + get: + return "" if stack.is_empty() else stack.back().char + + var _it: CharIterator + var _ed : EditorAdaptor + + func _init(ed: EditorAdaptor, from: Position): + _ed = ed + _it = ed.chars(from.line, from.column) + if not _it._iter_init(null): + eof = true + + func parse_until(to: Position) -> bool: + while valid and not eof: + parse_one_char() + if _it.line == to.line and _it.column == to.column: + break + return valid and not eof + + + func find_matching() -> Position: + var depth := len(stack) + while valid and not eof: + parse_one_char() + if len(stack) < depth: + return pos + return null + + func parse_one_char() -> String: # ChatGPT got credit here + if eof or not valid: + return "" + + var p := _it._iter_get(null) + pos = p + + if not _it._iter_next(null): + eof = true + + var char := p.char + var top: String = '' if stack.is_empty() else stack.back().char + if top in "'\"": # in string + if char == top and escape_count % 2 == 0: + stack.pop_back() + escape_count = 0 + return char + escape_count = escape_count + 1 if char == "\\" else 0 + elif in_comment: + if char == "\n": + in_comment = false + elif char == "#": + in_comment = true + elif char in open_symbol: + stack.append(p) + return char + elif char in close_symbol: + if top == close_symbol[char]: + stack.pop_back() + return char + else: + valid = false + return "" + + +class Register: + var line_wise : bool = false + var text : String: + get: + return DisplayServer.clipboard_get() + + func set_text(value: String, line_wise: bool) -> void: + self.line_wise = line_wise + DisplayServer.clipboard_set(value) + + +class BookmarkManager: + var bookmarks : Dictionary + + func on_lines_edited(from: int, to: int) -> void: + for b in bookmarks: + var line : int = bookmarks[b] + if line > from: + bookmarks[b] += to - from + + func set_bookmark(name: String, line: int) -> void: + bookmarks[name] = line + + func get_bookmark(name: String) -> int: + return bookmarks.get(name, -1) + + +class CommandMatchResult: + var full: Array[Dictionary] = [] + var partial: Array[Dictionary] = [] + + +class VimSession: + var ed : EditorAdaptor + + ## Mode insert_mode | visual_mode | visual_line + ## Insert true | false | false + ## Normal false | false | false + ## Visual false | true | false + ## Visual Line false | true | true + var insert_mode : bool = false + var visual_mode : bool = false + var visual_line : bool = false + + var normal_mode: bool: + get: + return not insert_mode and not visual_mode + + ## Pending input + var input_state := InputState.new() + + ## The last motion occurred + var last_motion : String + + ## When using jk for navigation, if you move from a longer line to a shorter line, the cursor may clip to the end of the shorter line. + ## If j is pressed again and cursor goes to the next line, the cursor should go back to its horizontal position on the longer + ## line if it can. This is to keep track of the horizontal position. + var last_h_pos : int = 0 + + ## How many times text are changed + var text_change_number : int + + ## List of positions for C-I and C-O + var jump_list := JumpList.new() + + ## The bookmark manager of the session + var bookmark_manager := BookmarkManager.new() + + ## The start position of visual mode + var visual_start_pos := Position.zero + + func enter_normal_mode() -> void: + if insert_mode: + ed.end_complex_operation() # Wrap up the undo operation when we get out of insert mode + + insert_mode = false + visual_mode = false + visual_line = false + ed.set_block_caret(true) + + func enter_insert_mode() -> void: + insert_mode = true + visual_mode = false + visual_line = false + ed.set_block_caret(false) + ed.begin_complex_operation() + + func enter_visual_mode(line_wise: bool) -> void: + insert_mode = false + visual_mode = true + visual_line = line_wise + + + visual_start_pos = ed.curr_position().right() + if line_wise: + ed.select(visual_start_pos.line + 1, 0, visual_start_pos.line, 0) + else: + ed.select_by_pos2(visual_start_pos.left(), visual_start_pos) + + +class Macro: + var keys : Array[InputEventKey] = [] + var enabled := false + + func _to_string() -> String: + var s := PackedStringArray() + for key in keys: + s.append(key.as_text_keycode()) + return ",".join(s) + + func play(ed: EditorAdaptor) -> void: + for key in keys: + ed.simulate_press_key(key) + ed.simulate_press(KEY_ESCAPE) + + +class MacroManager: + var vim : Vim + var macros : Dictionary = {} + var recording_name : String + var playing_names : Array[String] = [] + var command_buffer: Array[InputEventKey] + + func _init(vim: Vim): + self.vim = vim + + func start_record_macro(name: String): + print('Recording macro "%s"...' % name ) + macros[name] = Macro.new() + recording_name = name + + func stop_record_macro() -> void: + print('Stop recording macro "%s"' % recording_name) + macros[recording_name].enabled = true + recording_name = "" + + func is_recording() -> bool: + return recording_name != "" + + func play_macro(n: int, name: String, ed: EditorAdaptor) -> void: + var macro : Macro = macros.get(name, null) + if (macro == null or not macro.enabled): + return + if name in playing_names: + return # to avoid recursion + + playing_names.append(name) + if len(playing_names) == 1: + ed.begin_complex_operation() + + if DEBUGGING: + print("Playing macro %s: %s" % [name, macro]) + + for i in range(n): + macro.play(ed) + + ed.simulate_press(KEY_NONE, CODE_MACRO_PLAY_END) # This special marks the end of macro play + + func on_macro_finished(ed: EditorAdaptor): + var name : String = playing_names.pop_back() + if playing_names.is_empty(): + ed.end_complex_operation() + + func push_key(key: InputEventKey) -> void: + command_buffer.append(key) + if recording_name: + macros[recording_name].keys.append(key) + + func on_command_processed(command: Dictionary, is_edit: bool) -> void: + if is_edit and command.get('action', '') != "repeat_last_edit": + var macro := Macro.new() + macro.keys = command_buffer.duplicate() + macro.enabled = true + macros["."] = macro + command_buffer.clear() + + +## Global VIM state; has multiple sessions +class Vim: + var sessions : Dictionary + var current: VimSession + var register: Register = Register.new() + var last_char_search: Dictionary = {} # { selected_character, stop_before, forward, inclusive } + var last_search_command: String + var search_buffer: String + var macro_manager := MacroManager.new(self) + + func set_current_session(s: Script, ed: EditorAdaptor): + var session : VimSession = sessions.get(s) + if not session: + session = VimSession.new() + session.ed = ed + sessions[s] = session + current = session + + func remove_session(s: Script): + sessions.erase(s) + + +class CharIterator: + var ed : EditorAdaptor + var line : int + var column : int + var forward : bool + var one_line : bool + var line_text : String + + func _init(ed: EditorAdaptor, line: int, col: int, forward: bool, one_line: bool): + self.ed = ed + self.line = line + self.column = col + self.forward = forward + self.one_line = one_line + + func _ensure_column_valid() -> bool: + if column < 0 or column > len(line_text): + line += 1 if forward else -1 + if one_line or line < 0 or line > ed.last_line(): + return false + line_text = ed.line_text(line) + column = 0 if forward else len(line_text) + return true + + func _iter_init(arg) -> bool: + if line < 0 or line > ed.last_line(): + return false + line_text = ed.line_text(line) + return _ensure_column_valid() + + func _iter_next(arg) -> bool: + column += 1 if forward else -1 + return _ensure_column_valid() + + func _iter_get(arg) -> CharPos: + return CharPos.new(line_text, line, column) + + +class EditorAdaptor: + var code_editor: CodeEdit + var tab_width : int = 4 + var complex_ops : int = 0 + var vim: Vim + const MARGIN_LINES_UP: int = 6 + const MARGIN_LINES_DOWN: int = 4 + + func _init(v: Vim): + vim = v + + func set_code_editor(new_editor: CodeEdit) -> void: + self.code_editor = new_editor + + func notify_settings_changed(settings: EditorSettings) -> void: + tab_width = settings.get_setting("text_editor/behavior/indent/size") as int + + func curr_position() -> Position: + return Position.new(code_editor.get_caret_line(), code_editor.get_caret_column()) + + func curr_line() -> int: + return code_editor.get_caret_line() + + func curr_column() -> int: + var col = code_editor.get_caret_column() + if vim.current.visual_mode: + col -= 1 + return col + + func set_curr_column(col: int) -> void: + code_editor.set_caret_column(col) + + func jump_to(line: int, col: int) -> void: + code_editor.unfold_line(line) + update_margin(line) + code_editor.set_caret_line(line) + code_editor.set_caret_column(col) + + func update_margin(line: int) -> void: + var folded_lines_above = code_editor.get_next_visible_line_offset_from(line, -MARGIN_LINES_UP-1) - MARGIN_LINES_UP-1 + var folded_lines_below = code_editor.get_next_visible_line_offset_from(line, +MARGIN_LINES_DOWN+1) - MARGIN_LINES_DOWN-1 + if line < first_visible_line() + MARGIN_LINES_UP + folded_lines_above: + var lines_to_move = first_visible_line() - (first_visible_line() + MARGIN_LINES_UP) + line - folded_lines_above + code_editor.set_line_as_first_visible(max(0, lines_to_move)) + elif line > last_visible_line() - MARGIN_LINES_DOWN - folded_lines_below: + var lines_to_move = last_visible_line() - (last_visible_line() - MARGIN_LINES_DOWN) + line + folded_lines_below + code_editor.set_line_as_last_visible(min(last_line(), lines_to_move)) + + func first_line() -> int: + return 0 + + func last_line() -> int : + return code_editor.get_line_count() - 1 + + func first_visible_line() -> int: + return code_editor.get_first_visible_line() + + func last_visible_line() -> int: + return code_editor.get_last_full_visible_line() + + func get_visible_line_count(from_line: int, to_line: int) -> int: + return code_editor.get_visible_line_count_in_range(from_line, to_line) + + func next_unfolded_line(line: int, offset: int = 1, forward: bool = true) -> int: + var step : int = 1 if forward else -1 + if line + step > last_line() or line + step < first_line(): + return line + var count := code_editor.get_next_visible_line_offset_from(line + step, offset * step) + return line + count * (1 if forward else -1) + + func last_column(line: int = -1) -> int: + if line == -1: + line = curr_line() + return len(line_text(line)) - 1 + + func last_pos() -> Position: + var line = last_line() + return Position.new(line, last_column(line)) + + func line_text(line: int) -> String: + return code_editor.get_line(line) + + func range_text(range: TextRange) -> String: + var s := PackedStringArray() + for p in chars(range.from.line, range.from.column): + if p.equals(range.to): + break + s.append(p.char) + return "".join(s) + + func char_at(line: int, col: int) -> String: + var s := line_text(line) + return s[col] if col >= 0 and col < len(s) else '' + + func set_block_caret(block: bool) -> void: + if block: + if curr_column() == last_column() + 1: + code_editor.caret_type = TextEdit.CARET_TYPE_LINE + code_editor.add_theme_constant_override("caret_width", 8) + else: + code_editor.caret_type = TextEdit.CARET_TYPE_BLOCK + code_editor.add_theme_constant_override("caret_width", 1) + else: + code_editor.add_theme_constant_override("caret_width", 1) + code_editor.caret_type = TextEdit.CARET_TYPE_LINE + + func set_caret_blink(blink: bool) -> void: + code_editor.caret_blink = blink; + + func deselect() -> void: + code_editor.deselect() + + func select_range(r: TextRange) -> void: + select(r.from.line, r.from.column, r.to.line, r.to.column) + + func select_by_pos2(from: Position, to: Position) -> void: + select(from.line, from.column, to.line, to.column) + + func select(from_line: int, from_col: int, to_line: int, to_col: int) -> void: + if to_line > last_line(): # If we try to select pass the last line, select till the last char + to_line = last_line() + to_col = INF_COL + + code_editor.select(from_line, from_col, to_line, to_col) + + func delete_selection() -> void: + code_editor.delete_selection() + + func selected_text() -> String: + return code_editor.get_selected_text() + + func selection() -> TextRange: + var from : Position + var to : Position + if code_editor.has_selection(): + from = Position.new(code_editor.get_selection_from_line(), code_editor.get_selection_from_column()) + to = Position.new(code_editor.get_selection_to_line(), code_editor.get_selection_to_column()) + else: + from = Position.new(code_editor.get_caret_line(), code_editor.get_caret_column()) + to = Position.new(code_editor.get_caret_line(), code_editor.get_caret_column()) + return TextRange.new(from, to) + + func replace_selection(text: String) -> void: + var col := curr_column() + begin_complex_operation() + delete_selection() + insert_text(text) + end_complex_operation() + set_curr_column(col) + + func toggle_folding(line_or_above: int) -> void: + if code_editor.is_line_folded(line_or_above): + code_editor.unfold_line(line_or_above) + else: + while line_or_above >= 0: + if code_editor.can_fold_line(line_or_above): + code_editor.fold_line(line_or_above) + break + line_or_above -= 1 + + func fold_all() -> void: + code_editor.fold_all_lines() + + func unfold_all() -> void: + code_editor.unfold_all_lines() + + func insert_text(text: String) -> void: + code_editor.insert_text_at_caret(text) + + func offset_pos(pos: Position, offset: int) -> Position: + var count : int = abs(offset) + 1 + for p in chars(pos.line, pos.column, offset > 0): + count -= 1 + if count == 0: + return p + return null + + func undo() -> void: + code_editor.undo() + + func redo() -> void: + code_editor.redo() + + func indent() -> void: + code_editor.indent_lines() + + func unindent() -> void: + code_editor.unindent_lines() + + func simulate_press_key(key: InputEventKey): + for pressed in [true, false]: + var key2 := key.duplicate() + key2.pressed = pressed + Input.parse_input_event(key2) + + func simulate_press(keycode: Key, unicode: int=0, ctrl=false, alt=false, shift=false, meta=false) -> void: + var k = InputEventKey.new() + if ctrl: + k.ctrl_pressed = true + if shift: + k.shift_pressed = true + if alt: + k.alt_pressed = true + if meta: + k.meta_pressed = true + k.keycode = keycode + k.key_label = keycode + k.unicode = unicode + simulate_press_key(k) + + func begin_complex_operation() -> void: + complex_ops += 1 + if complex_ops == 1: + if DEBUGGING: + print("Complex operation begins") + code_editor.begin_complex_operation() + + func end_complex_operation() -> void: + complex_ops -= 1 + if complex_ops == 0: + if DEBUGGING: + print("Complex operation ends") + code_editor.end_complex_operation() + + ## Return the index of the first non whtie space character in string + func find_first_non_white_space_character(line: int) -> int: + var s := line_text(line) + return len(s) - len(s.lstrip(" \t\r\n")) + + ## Return the next (or previous) char from current position and update current position according. Return "" if not more char available + func chars(line: int, col: int, forward: bool = true, one_line: bool = false) -> CharIterator: + return CharIterator.new(self, line, col, forward, one_line) + + func find_forward(line: int, col: int, condition: Callable, one_line: bool = false) -> CharPos: + for p in chars(line, col, true, one_line): + if condition.call(p): + return p + return null + + func find_backforward(line: int, col: int, condition: Callable, one_line: bool = false) -> CharPos: + for p in chars(line, col, false, one_line): + if condition.call(p): + return p + return null + + func get_word_at_pos(line: int, col: int) -> TextRange: + var end := find_forward(line, col, func(p): return p.char not in ALPHANUMERIC, true); + var start := find_backforward(line, col, func(p): return p.char not in ALPHANUMERIC, true); + return TextRange.new(start.right(), end) + + func search(text: String, line: int, col: int, match_case: bool, whole_word: bool, forward: bool) -> Position: + var flags : int = 0 + if match_case: flags |= TextEdit.SEARCH_MATCH_CASE + if whole_word: flags |= TextEdit.SEARCH_WHOLE_WORDS + if not forward: flags |= TextEdit.SEARCH_BACKWARDS + var result = code_editor.search(text, flags, line, col) + if result.x < 0 or result. y < 0: + return null + + code_editor.set_search_text(text) + return Position.new(result.y, result.x) + + func has_focus() -> bool: + return weakref(code_editor).get_ref() and code_editor.has_focus() + + +class CommandDispatcher: + var key_map : Array[Dictionary] + + func _init(km: Array[Dictionary]): + self.key_map = km + + func dispatch(key: InputEventKey, vim: Vim, ed: EditorAdaptor) -> bool: + var key_code := key.as_text_keycode() + var input_state := vim.current.input_state + + vim.macro_manager.push_key(key) + + if key_code == "Escape": + input_state.clear() + vim.macro_manager.on_command_processed({}, vim.current.insert_mode) # From insert mode to normal mode, this marks the end of an edit command + vim.current.enter_normal_mode() + return false # Let godot get the Esc as well to dispose code completion pops, etc + + if vim.current.insert_mode: # We are in insert mode + return false # Let Godot CodeEdit handle it + + if key_code not in ["Shift", "Ctrl", "Alt", "Escape"]: # Don't add these to input buffer + # Handle digits + if key_code.is_valid_int() and input_state.buffer.is_empty(): + input_state.push_repeat_digit(key_code) + if input_state.get_repeat() > 0: # No more handding if it is only repeat digit + return true + + # Save key to buffer + input_state.push_key(key) + + # Match the command + var context = Context.VISUAL if vim.current.visual_mode else Context.NORMAL + var result = match_commands(context, vim.current.input_state, ed, vim) + if not result.full.is_empty(): + var command = result.full[0] + var change_num := vim.current.text_change_number + if process_command(command, ed, vim): + input_state.clear() + if vim.current.normal_mode: + vim.macro_manager.on_command_processed(command, vim.current.text_change_number > change_num) # Notify macro manager about the finished command + elif result.partial.is_empty(): + input_state.clear() + + return true # We handled the input + + func match_commands(context: Context, input_state: InputState, ed: EditorAdaptor, vim: Vim) -> CommandMatchResult: + # Partial matches are not applied. They inform the key handler + # that the current key sequence is a subsequence of a valid key + # sequence, so that the key buffer is not cleared. + var result := CommandMatchResult.new() + var pressed := input_state.key_codes() + + for command in key_map: + if not is_command_available(command, context, ed, vim): + continue + + var mapped : Array = command.keys + if mapped[-1] == "{char}": + if pressed.slice(0, -1) == mapped.slice(0, -1) and len(pressed) == len(mapped): + result.full.append(command) + elif mapped.slice(0, len(pressed)-1) == pressed.slice(0, -1): + result.partial.append(command) + else: + continue + else: + if pressed == mapped: + result.full.append(command) + elif mapped.slice(0, len(pressed)) == pressed: + result.partial.append(command) + else: + continue + + return result + + func is_command_available(command: Dictionary, context: Context, ed: EditorAdaptor, vim: Vim) -> bool: + if command.get("context") not in [null, context]: + return false + + var when : String = command.get("when", '') + if when and not Callable(Command, when).call(ed, vim): + return false + + var when_not: String = command.get("when_not", '') + if when_not and Callable(Command, when_not).call(ed, vim): + return false + + return true + + func process_command(command: Dictionary, ed: EditorAdaptor, vim: Vim) -> bool: + var vim_session := vim.current + var input_state := vim_session.input_state + var start := Position.new(ed.curr_line(), ed.curr_column()) + + # If there is an operator pending, then we do need a motion or operator (for linewise operation) + if not input_state.operator.is_empty() and (command.type != MOTION and command.type != OPERATOR): + return false + + if command.type == ACTION: + var action_args = command.get("action_args", {}) + if command.keys[-1] == "{char}": + action_args.selected_character = char(input_state.buffer.back().unicode) + process_action(command.action, action_args, ed, vim) + return true + elif command.type == MOTION or command.type == OPERATOR_MOTION: + var motion_args = command.get("motion_args", {}) + + if command.type == OPERATOR_MOTION: + var operator_args = command.get("operator_args", {}) + input_state.operator = command.operator + input_state.operator_args = operator_args + + if command.keys[-1] == "{char}": + motion_args.selected_character = char(input_state.buffer.back().unicode) + + var new_pos = process_motion(command.motion, motion_args, ed, vim) + if new_pos == null: + return true + + if vim_session.visual_mode: # Visual mode + start = vim_session.visual_start_pos.left() + if new_pos is TextRange: + start = new_pos.from # In some cases (text object), we need to override the start position + new_pos = new_pos.to + if vim_session.visual_line: + var start_line = start.line + var new_line = new_pos.line + var new_col = 0 + + if new_line == ed.last_line(): + new_col = ed.last_column(new_pos.line) + 1 + elif start.line >= new_pos.line: + start_line += 1 + + ed.select(start_line, 0, new_line, new_col) + else: # visual mode + ed.select_by_pos2(start, new_pos.right()) + elif input_state.operator.is_empty(): # Normal mode motion only + ed.jump_to(new_pos.line, new_pos.column) + else: # Normal mode operator motion + if new_pos is TextRange: + start = new_pos.from # In some cases (text object), we need to override the start position + new_pos = new_pos.to + var inclusive : bool = motion_args.get("inclusive", false) + ed.select_by_pos2(start, new_pos.right() if inclusive else new_pos) + process_operator(input_state.operator, input_state.operator_args, ed, vim) + return true + elif command.type == OPERATOR: + var operator_args = command.get("operator_args", {}) + if vim.current.visual_mode: + operator_args.line_wise = vim.current.visual_line + process_operator(command.operator, operator_args, ed, vim) + if operator_args.get("normal_mode_after_visual", true): + vim.current.enter_normal_mode() + return true + elif input_state.operator.is_empty(): # We are not fully done yet, need to wait for the motion + input_state.operator = command.operator + input_state.operator_args = operator_args + input_state.buffer.clear() + return false + else: + if input_state.operator == command.operator: # Line wise operation + operator_args.line_wise = true + var new_pos : Position = process_motion("expand_to_line", {}, ed, vim) + if new_pos.compares_to(start) > 0: + ed.select(start.line, 0, new_pos.line + 1, 0) + else: + ed.select(new_pos.line, 0, start.line + 1, 0) + process_operator(command.operator, operator_args, ed, vim) + return true + + return false + + func process_action(action: String, action_args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + if DEBUGGING: + print(" Action: %s %s" % [action, action_args]) + + action_args.repeat = max(1, vim.current.input_state.get_repeat()) + Callable(Command, action).call(action_args, ed, vim) + + if vim.current.visual_mode and action != "enter_visual_mode": + vim.current.enter_normal_mode() + + func process_operator(operator: String, operator_args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: + if DEBUGGING: + print(" Operator %s %s on %s" % [operator, operator_args, ed.selection()]) + + # Perform operation + Callable(Command, operator).call(operator_args, ed, vim) + + + func process_motion(motion: String, motion_args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Variant: + # Get current position + var cur := Position.new(ed.curr_line(), ed.curr_column()) + + # Prepare motion args + var user_repeat = vim.current.input_state.get_repeat() + if user_repeat > 0: + motion_args.repeat = user_repeat + motion_args.repeat_is_explicit = true + else: + motion_args.repeat = 1 + motion_args.repeat_is_explicit = false + + # Calculate new position + var result = Callable(Command, motion).call(cur, motion_args, ed, vim) + if result is Position: + var new_pos : Position = result + if new_pos.column == INF_COL: # INF_COL means the last column + new_pos.column = ed.last_column(new_pos.line) + + if motion_args.get('to_jump_list', false): + vim.current.jump_list.add(cur, new_pos) + + # Save last motion + vim.current.last_motion = motion + + if DEBUGGING: + print(" Motion: %s %s to %s" % [motion, motion_args, result]) + + return result diff --git a/langtons-ant/addons/godot-vim/godot-vim.gd.uid b/langtons-ant/addons/godot-vim/godot-vim.gd.uid new file mode 100644 index 0000000..f02d030 --- /dev/null +++ b/langtons-ant/addons/godot-vim/godot-vim.gd.uid @@ -0,0 +1 @@ +uid://t4imnbu2n5vw diff --git a/langtons-ant/addons/godot-vim/icon.svg b/langtons-ant/addons/godot-vim/icon.svg new file mode 100644 index 0000000..9831b04 --- /dev/null +++ b/langtons-ant/addons/godot-vim/icon.svg @@ -0,0 +1,179 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/langtons-ant/addons/godot-vim/plugin.cfg b/langtons-ant/addons/godot-vim/plugin.cfg new file mode 100644 index 0000000..da5f41c --- /dev/null +++ b/langtons-ant/addons/godot-vim/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="godot-vim" +description="VIM bindings for godot4" +author="Josh N" +version="0.3" +script="godot-vim.gd" diff --git a/langtons-ant/addons/godot_vim/command_line.gd b/langtons-ant/addons/godot_vim/command_line.gd new file mode 100644 index 0000000..a94cbac --- /dev/null +++ b/langtons-ant/addons/godot_vim/command_line.gd @@ -0,0 +1,86 @@ +extends LineEdit + +const Cursor = preload("res://addons/godot_vim/cursor.gd") +const StatusBar = preload("res://addons/godot_vim/status_bar.gd") +const Constants = preload("res://addons/godot_vim/constants.gd") +const Dispatcher = preload("res://addons/godot_vim/dispatcher.gd") +const Mode = Constants.Mode + +const Marks = preload("res://addons/godot_vim/commands/marks.gd") +const Goto = preload("res://addons/godot_vim/commands/goto.gd") +const Find = preload("res://addons/godot_vim/commands/find.gd") + +var code_edit: CodeEdit +var cursor: Cursor +var status_bar: StatusBar +var globals: Dictionary +var dispatcher: Dispatcher + +var is_paused: bool = false +var search_pattern: String = '' + +func _ready(): + dispatcher = Dispatcher.new() + dispatcher.globals = globals + placeholder_text = "Enter command..." + show() + + text_submitted.connect(_on_text_submitted) + text_changed.connect(_on_text_changed) + editable = true + +func set_command(cmd: String): + text = cmd + caret_column = text.length() + +func _on_text_changed(cmd: String): + if !cmd.begins_with('/'): return + var pattern: String = cmd.substr(1) + var rmatch: RegExMatch = globals.vim_plugin.search_regex( + code_edit, + pattern, + cursor.get_caret_pos() + Vector2i.RIGHT + ) + if rmatch == null: + code_edit.remove_secondary_carets() + return + var pos: Vector2i = globals.vim_plugin.idx_to_pos(code_edit, rmatch.get_start()) + if code_edit.get_caret_count() < 2: + code_edit.add_caret(pos.y, pos.x) + code_edit.select(pos.y, pos.x, pos.y, pos.x + rmatch.get_string().length(), 1) + code_edit.scroll_vertical = code_edit.get_scroll_pos_for_line(pos.y) + +func handle_command(cmd: String): + if cmd.begins_with('/'): + var find = Find.new() + find.execute(globals, cmd) + return + + if cmd.trim_prefix(':').is_valid_int(): + var goto = Goto.new() + goto.execute(globals, cmd.trim_prefix(':')) + return + + if dispatcher.dispatch(cmd) == OK: + set_paused(true) + return + + status_bar.display_error('Unknown command: "%s"' % [ cmd.trim_prefix(':') ]) + set_paused(true) + +func close(): + hide() + clear() + set_paused(false) + +func set_paused(paused: bool): + is_paused = paused + text = "Press ENTER to continue" if is_paused else "" + editable = !is_paused + +func _on_text_submitted(new_text: String): + if is_paused: + cursor.set_mode(Mode.NORMAL) + status_bar.main_label.text = '' + return + handle_command(new_text) diff --git a/langtons-ant/addons/godot_vim/command_line.gd.uid b/langtons-ant/addons/godot_vim/command_line.gd.uid new file mode 100644 index 0000000..b415696 --- /dev/null +++ b/langtons-ant/addons/godot_vim/command_line.gd.uid @@ -0,0 +1 @@ +uid://lk5et75ba1pr diff --git a/langtons-ant/addons/godot_vim/commands/find.gd b/langtons-ant/addons/godot_vim/commands/find.gd new file mode 100644 index 0000000..9a9cd8a --- /dev/null +++ b/langtons-ant/addons/godot_vim/commands/find.gd @@ -0,0 +1,16 @@ +const Constants = preload("res://addons/godot_vim/constants.gd") +const Mode = Constants.Mode + +func execute(api : Dictionary, args: String): + api.command_line.search_pattern = args.substr(1) + var rmatch: RegExMatch = api.vim_plugin.search_regex( + api.code_edit, + api.command_line.search_pattern, + api.cursor.get_caret_pos() + Vector2i.RIGHT + ) + if rmatch != null: + var pos: Vector2i = api.vim_plugin.idx_to_pos(api.code_edit, rmatch.get_start()) + api.cursor.set_caret_pos(pos.y, pos.x) + else: + api.status_bar.display_error('Pattern not found: "%s"' % [api.command_line.search_pattern]) + api.cursor.set_mode(Mode.NORMAL) diff --git a/langtons-ant/addons/godot_vim/commands/find.gd.uid b/langtons-ant/addons/godot_vim/commands/find.gd.uid new file mode 100644 index 0000000..69a291a --- /dev/null +++ b/langtons-ant/addons/godot_vim/commands/find.gd.uid @@ -0,0 +1 @@ +uid://dp7ck72w5b66n diff --git a/langtons-ant/addons/godot_vim/commands/goto.gd b/langtons-ant/addons/godot_vim/commands/goto.gd new file mode 100644 index 0000000..22a62a1 --- /dev/null +++ b/langtons-ant/addons/godot_vim/commands/goto.gd @@ -0,0 +1,6 @@ +const Constants = preload("res://addons/godot_vim/constants.gd") +const Mode = Constants.Mode + +func execute(api, args): + api.cursor.set_caret_pos(args.to_int(), 0) + api.cursor.set_mode(Mode.NORMAL) diff --git a/langtons-ant/addons/godot_vim/commands/goto.gd.uid b/langtons-ant/addons/godot_vim/commands/goto.gd.uid new file mode 100644 index 0000000..baeea78 --- /dev/null +++ b/langtons-ant/addons/godot_vim/commands/goto.gd.uid @@ -0,0 +1 @@ +uid://cu4md7kr2qrp diff --git a/langtons-ant/addons/godot_vim/commands/marks.gd b/langtons-ant/addons/godot_vim/commands/marks.gd new file mode 100644 index 0000000..c6d142a --- /dev/null +++ b/langtons-ant/addons/godot_vim/commands/marks.gd @@ -0,0 +1,29 @@ +const Contants = preload("res://addons/godot_vim/constants.gd") +const StatusBar = preload("res://addons/godot_vim/status_bar.gd") +const Mode = Contants.Mode + +func execute(api, _args): + var marks: Dictionary = api.get('marks', {}) + if marks.is_empty(): + api.status_bar.display_error("No marks set") + api.cursor.set_mode(Mode.NORMAL) + return + + var display_mark = func(key: String, m: Dictionary) -> String: + var pos: Vector2i = m.get('pos', Vector2i()) + var file: String = m.get('file', '') + return "\n%s\t\t%s \t%s \t\t %s" % [key, pos.y, pos.x, file] + + var text: String = "[color=%s]List of all marks[/color]\nmark\tline\tcol \t file" % StatusBar.SPECIAL_COLOR + for key in marks.keys(): + var unicode: int = key.unicode_at(0) + if (unicode < 65 or unicode > 90) and (unicode < 97 or unicode > 122): + continue + text += display_mark.call(key, marks[key]) + for key in marks.keys(): + var unicode: int = key.unicode_at(0) + if (unicode >= 65 and unicode <= 90) or (unicode >= 97 and unicode <= 122) or key == "-1": + continue + text += display_mark.call(key, marks[key]) + + api.status_bar.display_text(text, Control.TEXT_DIRECTION_LTR) diff --git a/langtons-ant/addons/godot_vim/commands/marks.gd.uid b/langtons-ant/addons/godot_vim/commands/marks.gd.uid new file mode 100644 index 0000000..b138ae5 --- /dev/null +++ b/langtons-ant/addons/godot_vim/commands/marks.gd.uid @@ -0,0 +1 @@ +uid://cf1an2q01i3ix diff --git a/langtons-ant/addons/godot_vim/constants.gd b/langtons-ant/addons/godot_vim/constants.gd new file mode 100644 index 0000000..c94d72a --- /dev/null +++ b/langtons-ant/addons/godot_vim/constants.gd @@ -0,0 +1,8 @@ +enum Mode { NORMAL, INSERT, VISUAL, VISUAL_LINE, COMMAND } + +# Used for commands like "w" "b" and "e" respectively +enum WordEdgeMode { WORD, BEGINNING, END } + +const SPACES: String = " \t" +const KEYWORDS: String = ".,\"'-=+!@#$%^&*()[]{}?~/\\<>:;" +const DIGITS: String = "0123456789" diff --git a/langtons-ant/addons/godot_vim/constants.gd.uid b/langtons-ant/addons/godot_vim/constants.gd.uid new file mode 100644 index 0000000..86c1875 --- /dev/null +++ b/langtons-ant/addons/godot_vim/constants.gd.uid @@ -0,0 +1 @@ +uid://d2ro7q1ec5gk0 diff --git a/langtons-ant/addons/godot_vim/cursor.gd b/langtons-ant/addons/godot_vim/cursor.gd new file mode 100644 index 0000000..80647ad --- /dev/null +++ b/langtons-ant/addons/godot_vim/cursor.gd @@ -0,0 +1,722 @@ +extends Control + +const CommandLine = preload("res://addons/godot_vim/command_line.gd") +const StatusBar = preload("res://addons/godot_vim/status_bar.gd") +const Constants = preload("res://addons/godot_vim/constants.gd") +const Mode = Constants.Mode +const WordEdgeMode = Constants.WordEdgeMode +const KEYWORDS = Constants.KEYWORDS +const SPACES = Constants.SPACES + +var code_edit: CodeEdit +var command_line: CommandLine +var status_bar: StatusBar + +var mode: Mode = Mode.NORMAL +var caret: Vector2 +var input_stream: String = "" +var selection_from: Vector2i = Vector2i() # For visual modes +var selection_to: Vector2i = Vector2i() # For visual modes +var globals: Dictionary = {} + +func _init(): + set_focus_mode(FOCUS_ALL) + +func _ready(): + code_edit.connect("focus_entered", focus_entered) + code_edit.connect("caret_changed", cursor_changed) + call_deferred('set_mode', Mode.NORMAL) + +func cursor_changed(): + draw_cursor() + +func focus_entered(): + if mode == Mode.NORMAL: + code_edit.release_focus() + self.grab_focus() + + +func reset_normal(): + code_edit.cancel_code_completion() + input_stream = '' + set_mode(Mode.NORMAL) + selection_from = Vector2i.ZERO + selection_to = Vector2i.ZERO + set_column(code_edit.get_caret_column()) + return + + +func back_to_normal_mode(event, m): + var old_caret_pos = code_edit.get_caret_column() + if Input.is_key_pressed(KEY_ESCAPE): + if m == Mode.INSERT: + handle_input_stream('l') + reset_normal() + return 1 + if m == Mode.INSERT: + var old_time = Time.get_ticks_msec() + if Input.is_key_label_pressed(KEY_J): + old_caret_pos = code_edit.get_caret_column() + if Time.get_ticks_msec() - old_time < 700 and Input.is_key_label_pressed(KEY_K): + code_edit.backspace() + code_edit.cancel_code_completion() + reset_normal() + handle_input_stream('l') + return 1 + return 0 + + +func _input(event): + if back_to_normal_mode(event, mode): return + draw_cursor() + code_edit.cancel_code_completion() + if !has_focus(): return + if !event is InputEventKey: return + if !event.pressed: return + if mode == Mode.INSERT or mode == Mode.COMMAND: return + + if event.keycode == KEY_ESCAPE: + input_stream = '' + return + + + var ch: String + if !event is InputEventMouseMotion and !event is InputEventMouseButton: + ch = char(event.unicode) + + if Input.is_key_pressed(KEY_ENTER): + ch = '' + if Input.is_key_pressed(KEY_TAB): + ch = '' + if Input.is_key_pressed(KEY_CTRL): + if OS.is_keycode_unicode(event.keycode): + var c: String = char(event.keycode) + if !Input.is_key_pressed(KEY_SHIFT): + c = c.to_lower() + ch = '' % c + + input_stream += ch + status_bar.display_text(input_stream) + + var s: int = globals.vim_plugin.get_first_non_digit_idx(input_stream) + if s == -1: return # All digits + + var cmd: String = input_stream.substr(s) + var count: int = maxi( input_stream.left(s).to_int(), 1 ) + for i in count: + input_stream = handle_input_stream(cmd) + + +func handle_input_stream(stream: String) -> String: + # BEHOLD, THE IF STATEMENT HELL!!! MUAHAHAHAHa + if stream == 'h': + move_column(-1) + return '' + if stream == 'j': + move_line(+1) + return '' + if stream == 'k': + move_line(-1) + return '' + if stream == 'l': + move_column(+1) + return '' + if stream.to_lower().begins_with('w'): + var p: Vector2i = get_word_edge_pos(get_line(), get_column(), '' if stream[0] == 'W' else KEYWORDS, WordEdgeMode.WORD) + set_caret_pos(p.y, p.x) + return '' + if stream.to_lower().begins_with('e'): + var p: Vector2i = get_word_edge_pos(get_line(), get_column(), '' if stream[0] == 'E' else KEYWORDS, WordEdgeMode.END) + set_caret_pos(p.y, p.x) + return '' + if stream.to_lower().begins_with('b'): + var p: Vector2i = get_word_edge_pos(get_line(), get_column(), '' if stream[0] == 'B' else KEYWORDS, WordEdgeMode.BEGINNING) + set_caret_pos(p.y, p.x) + return '' + + if stream.to_lower() .begins_with('f') or stream.to_lower() .begins_with('t'): + if stream.length() == 1: return stream + + var char: String = stream[1] # TODO check for , and + globals.last_search = stream.left(2) # First 2 in case it's longer + var col: int = find_char_motion(get_line(), get_column(), stream[0], char) + if col >= 0: + set_column(col) + return '' + if stream.begins_with(';') and globals.has('last_search'): + var cmd: String = globals.last_search[0] + var col: int = find_char_motion(get_line(), get_column(), cmd, globals.last_search[1]) + if col >= 0: + set_column(col) + return '' + if stream.begins_with(',') and globals.has('last_search'): + var cmd: String = globals.last_search[0] + cmd = cmd.to_upper() if is_lowercase(cmd) else cmd.to_lower() + var col: int = find_char_motion(get_line(), get_column(), cmd, globals.last_search[1]) + if col >= 0: + set_column(col) + return '' + + if mode == Mode.VISUAL: # TODO make it work for visual line too + var range: Array = calc_double_motion_region(selection_to, stream) + if range.size() == 1: return stream + if range.size() == 2: + selection_from = range[0] + selection_to = range[1] + update_visual_selection() + + if stream.begins_with('J') and mode == Mode.NORMAL: + code_edit.begin_complex_operation() + code_edit.select( get_line(), get_line_length(), get_line()+1, code_edit.get_first_non_whitespace_column(get_line()+1) ) + code_edit.delete_selection() + code_edit.deselect() + code_edit.insert_text_at_caret(' ') + code_edit.end_complex_operation() + globals.last_command = stream + return '' + if stream.begins_with('d'): + if is_mode_visual(mode): + DisplayServer.clipboard_set( '\r' + code_edit.get_selected_text() ) + code_edit.delete_selection() + move_line(+1) + set_mode(Mode.NORMAL) + return '' + + if stream.begins_with('dd') and mode == Mode.NORMAL: + code_edit.select( get_line()-1, get_line_length(get_line()-1), get_line(), get_line_length() ) + DisplayServer.clipboard_set( '\r' + code_edit.get_selected_text() ) + code_edit.delete_selection() + move_line(+1) + globals.last_command = stream + return '' + + var range: Array = calc_double_motion_region(get_caret_pos(), stream, 1) + if range.size() == 0: return '' + if range.size() == 1: return stream + if range.size() == 2: + code_edit.select(range[0].y, range[0].x, range[1].y, range[1].x + 1) + code_edit.cut() + globals.last_command = stream + return '' + + if mode == Mode.NORMAL and stream.begins_with('D'): + code_edit.select( get_line(), code_edit.get_caret_column(), get_line(), get_line_length() ) + code_edit.cut() + globals.last_command = stream + return '' + if stream.begins_with('p'): + code_edit.begin_complex_operation() + if is_mode_visual(mode): + code_edit.delete_selection() + if DisplayServer.clipboard_get().begins_with('\r\n'): + set_column(get_line_length()) + else: + move_column(+1) + code_edit.deselect() + code_edit.paste() + move_column(-1) + code_edit.end_complex_operation() + set_mode(Mode.NORMAL) + globals.last_command = stream + return '' + if stream.begins_with('P'): + status_bar.display_error("Unimplemented command: P") + return '' + if stream.begins_with('$'): + set_column(get_line_length()) + return '' + if stream.begins_with('^'): + set_column( code_edit.get_first_non_whitespace_column(get_line()) ) + return '' + if stream == 'G': + set_line(code_edit.get_line_count()) + return '' + if stream.begins_with('g'): + if stream.begins_with('gg'): + set_line(0) + return '' + + if stream.begins_with('gc') and is_mode_visual(mode): + code_edit.begin_complex_operation() + for line in range( min(selection_from.y, selection_to.y), max(selection_from.y, selection_to.y)+1 ): + toggle_comment(line) + code_edit.end_complex_operation() + set_mode(Mode.NORMAL) + return '' + if stream.begins_with('gcc') and mode == Mode.NORMAL: + toggle_comment(get_line()) + globals.last_command = stream + return '' + return stream + + if stream == '0': + set_column(0) + return '' + if stream == 'i' and mode == Mode.NORMAL: + set_mode(Mode.INSERT) + return '' + if stream == 'a' and mode == Mode.NORMAL: + set_mode(Mode.INSERT) + move_column(+1) + return '' + if stream == 'I' and mode == Mode.NORMAL: + set_column(code_edit.get_first_non_whitespace_column(get_line())) + set_mode(Mode.INSERT) + return '' + if stream.begins_with('A') and mode == Mode.NORMAL: + set_mode(Mode.INSERT) + set_column(get_line_length()) + return '' + if stream == 'v': + set_mode(Mode.VISUAL) + return '' + if stream == 'V': + set_mode(Mode.VISUAL_LINE) + return '' + if stream.begins_with('o'): + if is_mode_visual(mode): + var tmp: Vector2i = selection_from + selection_from = selection_to + selection_to = tmp + return '' + + var ind: int = code_edit.get_first_non_whitespace_column(get_line()) + if code_edit.get_line(get_line()).ends_with(':'): + ind += 1 + var line: int = code_edit.get_caret_line() + code_edit.insert_line_at(line + int(line < code_edit.get_line_count() - 1), "\t".repeat(ind)) + move_line(+1) + set_column(ind) + set_mode(Mode.INSERT) + globals.last_command = stream + return '' + if stream.begins_with('O') and mode == Mode.NORMAL: + var ind: int = code_edit.get_first_non_whitespace_column(get_line()) + code_edit.insert_line_at(get_line(), "\t".repeat(ind)) + move_line(-1) + set_column(ind) + set_mode(Mode.INSERT) + globals.last_command = stream + return '' + + if stream == 'x': + code_edit.copy() + code_edit.delete_selection() + globals.last_command = stream + return '' + if stream.begins_with('s'): + code_edit.cut() + set_mode(Mode.INSERT) + return '' + if stream == 'u': + code_edit.undo() + set_mode(Mode.NORMAL) + return '' + if stream.begins_with(''): + code_edit.redo() + return '' + if stream.begins_with('r') and mode == Mode.NORMAL: + if stream.length() < 2: return stream + code_edit.begin_complex_operation() + code_edit.delete_selection() + var ch: String = stream[1] + if stream.substr(1).begins_with(''): + ch = '\n' + elif stream.substr(1).begins_with(''): + ch = '\t' + code_edit.insert_text_at_caret(ch) + move_column(-1) + code_edit.end_complex_operation() + globals.last_command = stream + return '' + if stream.begins_with('y'): + if is_mode_visual(mode): + code_edit.copy() + set_mode(Mode.NORMAL) + return '' + + if stream.length() == 1: return stream + if stream.begins_with('yy') and mode == Mode.NORMAL: + code_edit.select(code_edit.get_caret_line(), 0, code_edit.get_caret_line(), get_line_length()) + DisplayServer.clipboard_set( '\r\n' + code_edit.get_selected_text() ) + move_column(0) + code_edit.deselect() + + var range: Array = calc_double_motion_region(get_caret_pos(), stream, 1) + if range.size() == 0: return '' + if range.size() == 1: return stream + if range.size() == 2: + code_edit.select(range[0].y, range[0].x, range[1].y, range[1].x + 1) + code_edit.copy() + code_edit.deselect() + return '' + + if stream == '.': + if globals.has('last_command'): + handle_input_stream(globals.last_command) + call_deferred(&'set_mode', Mode.NORMAL) + return '' + + if stream.begins_with(':') and mode == Mode.NORMAL: # Could make this work with visual too ig + set_mode(Mode.COMMAND) + command_line.set_command(':') + return '' + if stream.begins_with('/') and mode == Mode.NORMAL: + set_mode(Mode.COMMAND) + command_line.set_command('/') + return '' + if stream.begins_with('n'): + var rmatch: RegExMatch = globals.vim_plugin.search_regex( + code_edit, + command_line.search_pattern, + get_caret_pos() + Vector2i.RIGHT + ) + if rmatch != null: + var pos: Vector2i = globals.vim_plugin.idx_to_pos(code_edit,rmatch.get_start()) + set_caret_pos(pos.y, pos.x) + return '' + if stream.begins_with('N'): + var rmatch: RegExMatch = globals.vim_plugin.search_regex_backwards( + code_edit, + command_line.search_pattern, + get_caret_pos() + Vector2i.LEFT + ) + if rmatch != null: + var pos: Vector2i = globals.vim_plugin.idx_to_pos(code_edit,rmatch.get_start()) + set_caret_pos(pos.y, pos.x) + return '' + + if stream.begins_with('c'): + if mode == Mode.VISUAL: + code_edit.cut() + set_mode(Mode.INSERT) + return '' + + if stream.begins_with('cc') and mode == Mode.NORMAL: + code_edit.begin_complex_operation() + var l: int = get_line() + var ind: int = code_edit.get_first_non_whitespace_column(l) + code_edit.select( l-1, get_line_length(l-1), l, get_line_length(l) ) + code_edit.cut() + code_edit.insert_line_at(get_line()+1, "\t".repeat(ind)) + code_edit.end_complex_operation() + move_line(+1) + set_mode(Mode.INSERT) + globals.last_command = stream + return '' + + var range: Array = calc_double_motion_region(get_caret_pos(), stream, 1) + if range.size() == 0: return '' + if range.size() == 1: return stream + if range.size() == 2: + code_edit.select(range[0].y, range[0].x, range[1].y, range[1].x + 1) + code_edit.cut() + set_mode(Mode.INSERT) + globals.last_command = stream + return '' + if mode == Mode.NORMAL and stream.begins_with('C'): + code_edit.select( get_line(), code_edit.get_caret_column(), get_line(), get_line_length() ) + code_edit.cut() + set_mode(Mode.INSERT) + globals.last_command = stream + return '' + if stream.begins_with('z'): + if stream.begins_with('zz') and mode == Mode.NORMAL: + code_edit.center_viewport_to_caret() + return '' + return stream + + if stream.begins_with('>'): + if is_mode_visual(mode) and stream.length() == 1: + code_edit.indent_lines() + return '' + if stream.length() == 1: return stream + if stream.begins_with('>>') and mode == Mode.NORMAL: + code_edit.indent_lines() + globals.last_command = stream + return '' + if stream.begins_with('<'): + if is_mode_visual(mode) and stream.length() == 1: + code_edit.unindent_lines() + return '' + if stream.length() == 1: return stream + if stream.begins_with('<<') and mode == Mode.NORMAL: + code_edit.unindent_lines() + globals.last_command = stream + return '' + + if stream.begins_with('}'): + var para_edge: Vector2i = get_paragraph_edge_pos( get_line(), 1 ) + set_caret_pos(para_edge.y, para_edge.x) + return '' + + if stream.begins_with('{'): + var para_edge: Vector2i = get_paragraph_edge_pos( get_line(), -1 ) + set_caret_pos(para_edge.y, para_edge.x) + return '' + + if stream.begins_with('m') and mode == Mode.NORMAL: + if stream.length() < 2: return stream + if !globals.has('marks'): globals.marks = {} + var m: String = stream[1] + var unicode: int = m.unicode_at(0) + if (unicode < 65 or unicode > 90) and (unicode < 97 or unicode > 122): + status_bar.display_error('Marks must be between a-z or A-Z') + return '' + globals.marks[m] = { + 'file' : globals.script_editor.get_current_script().resource_path, + 'pos' : Vector2i(code_edit.get_caret_column(), code_edit.get_caret_line()) + } + status_bar.display_text('Mark "%s" set' % m, TEXT_DIRECTION_LTR) + return '' + if stream.begins_with('`'): + if stream.length() < 2: return stream + if !globals.has('marks'): globals.marks = {} + if !globals.marks.has(stream[1]): + status_bar.display_error('Mark "%s" not set' % [ stream[1] ]) + return '' + var mark: Dictionary = globals.marks[stream[1]] + globals.vim_plugin.edit_script(mark.file, mark.pos) + return '' + return '' + + +# Mostly used for commands like "w", "b", and "e" +# delims is the keywords / characters used as delimiters. Usually, it's the constant KEYWORDS +func get_word_edge_pos(from_line: int, from_col: int, delims: String, mode: WordEdgeMode) -> Vector2i: + var search_dir: int = -1 if mode == WordEdgeMode.BEGINNING else 1 + var char_offset: int = 1 if mode == WordEdgeMode.END else -1 + var line: int = from_line + var col: int = from_col + search_dir + var text: String = get_line_text(line) + + while line >= 0 and line < code_edit.get_line_count(): + while col >= 0 and col < text.length(): + var char: String = text[col] + if SPACES.contains(char): + col += search_dir + continue + # Please don't question this lmao. It just works, alight? + var other_char: String = ' ' if col == (text.length()-1) * int(char_offset > 0) else text[col + char_offset] + + if SPACES.contains(other_char): + return Vector2i(col, line) + if delims.contains(char) != delims.contains(other_char): + return Vector2i(col, line) + col += search_dir + line += search_dir + text = get_line_text(line) + col = (text.length() - 1) * int(search_dir < 0 and char_offset < 0) + return Vector2i(from_col, from_line) + +func get_paragraph_edge_pos(from_line: int, search_dir: int): + var line: int = from_line + var prev_empty: bool = code_edit.get_line(line) .strip_edges().is_empty() + line += search_dir + while line >= 0 and line < code_edit.get_line_count(): + var text: String = code_edit.get_line(line) .strip_edges() + if text.is_empty() and !prev_empty: + return Vector2i(text.length(), line) + prev_empty = text.is_empty() + line += search_dir + return Vector2i(0, line) + +# motion: command like "f", "t", "F", or "T" +func find_char_motion(in_line: int, from_col: int, motion: String, char: String) -> int: + var search_dir: int = 1 if is_lowercase(motion) else -1 + var offset: int = int(motion == 'T') - int(motion == 't') # 1 if T, -1 if t, 0 otherwise + var text: String = get_line_text(in_line) + + var col: int = -1 + if motion == 'f' or motion == 't': + col = text.find(char, from_col + search_dir) + elif motion == 'F' or motion == 'T': + col = text.rfind(char, from_col + search_dir) + if col == -1: + return -1 + return col + offset + +# returns: [ Vector2i from_pos, Vector2i to_pos ] +func calc_double_motion_region(from_pos: Vector2i, stream: String, from_char: int = 0) -> Array[Vector2i]: + var primary: String = get_stream_char(stream, from_char) + var secondary: String = get_stream_char(stream, from_char + 1) + if primary == '': + return [from_pos] # Incomplete + + if primary.to_lower() == 'w': + var p1: Vector2i = get_word_edge_pos(from_pos.y, from_pos.x, '' if primary == 'W' else KEYWORDS, WordEdgeMode.WORD) + return [from_pos, p1 + Vector2i.LEFT] + if primary.to_lower() == 'b': + var p0: Vector2i = get_word_edge_pos(from_pos.y, from_pos.x, '' if primary == 'B' else KEYWORDS, WordEdgeMode.BEGINNING) + return [p0, from_pos + Vector2i.LEFT] + if primary.to_lower() == 'e': + var p1: Vector2i = get_word_edge_pos(from_pos.y, from_pos.x, '' if primary == 'E' else KEYWORDS, WordEdgeMode.END) + return [from_pos, p1] + + if primary == '$': + var p1: Vector2i = Vector2i(get_line_length(from_pos.y), from_pos.y) + return [from_pos, p1] + if primary == '^': + var p0: Vector2i = Vector2i(code_edit.get_first_non_whitespace_column(from_pos.y), from_pos.y) + return [p0, from_pos + Vector2i.LEFT] + + if primary != 'i' and primary != 'a': + return [] # Invalid + if secondary == '': + return [from_pos] # Incomplete + + if primary == 'i' and secondary.to_lower() == 'w': + var p0: Vector2i = get_word_edge_pos(from_pos.y, from_pos.x + 1, '' if secondary == 'W' else KEYWORDS, WordEdgeMode.BEGINNING) + var p1: Vector2i = get_word_edge_pos(from_pos.y, from_pos.x - 1, '' if secondary == 'W' else KEYWORDS, WordEdgeMode.END) + return [ p0, p1 ] + + if primary == 'i' and secondary == 'p': + var p0: Vector2i = get_paragraph_edge_pos(from_pos.y + 1, -1) + Vector2i.DOWN + var p1: Vector2i = get_paragraph_edge_pos(from_pos.y - 1, 1) + return [ p0, p1 ] + + return [] # Unknown combination + +func toggle_comment(line: int): + var ind: int = code_edit.get_first_non_whitespace_column(line) + var text: String = get_line_text(line) + # Comment line + if text[ind] != '#': + code_edit.set_line(line, text.insert(ind, '# ')) + return + # Uncomment line + var start_col: int = get_word_edge_pos(line, ind, KEYWORDS, WordEdgeMode.WORD).x + code_edit.select(line, ind, line, start_col) + code_edit.delete_selection() + +func set_mode(m: int): + var old_mode: int = mode + mode = m + command_line.close() + match mode: + Mode.NORMAL: + code_edit.remove_secondary_carets() + code_edit.deselect() + code_edit.release_focus() + code_edit.deselect() + self.grab_focus() + status_bar.set_mode_text(Mode.NORMAL) + if old_mode == Mode.INSERT: + move_column(-1) + Mode.VISUAL: + if old_mode != Mode.VISUAL_LINE: + selection_from = Vector2i(code_edit.get_caret_column(), code_edit.get_caret_line()) + selection_to = Vector2i(code_edit.get_caret_column(), code_edit.get_caret_line()) + set_caret_pos(selection_to.y, selection_to.x) + status_bar.set_mode_text(Mode.VISUAL) + Mode.VISUAL_LINE: + if old_mode != Mode.VISUAL: + selection_from = Vector2i(code_edit.get_caret_column(), code_edit.get_caret_line()) + selection_to = Vector2i(code_edit.get_caret_column(), code_edit.get_caret_line()) + set_caret_pos(selection_to.y, selection_to.x) + status_bar.set_mode_text(Mode.VISUAL_LINE) + Mode.COMMAND: + command_line.show() + command_line.call_deferred("grab_focus") + status_bar.set_mode_text(Mode.COMMAND) + Mode.INSERT: + code_edit.call_deferred("grab_focus") + status_bar.set_mode_text(Mode.INSERT) + _: + pass + +func move_line(offset:int): + set_line(get_line() + offset) + +func get_line() -> int: + if is_mode_visual(mode): + return selection_to.y + return code_edit.get_caret_line() + +func get_line_text(line: int = -1) -> String: + if line == -1: + return code_edit.get_line(get_line()) + return code_edit.get_line(line) + +func get_line_length(line: int = -1) -> int: + return get_line_text(line).length() + +func set_caret_pos(line: int, column: int): + set_line(line) # line has to be set before column + set_column(column) + +func get_caret_pos() -> Vector2i: + return Vector2i(code_edit.get_caret_column(), code_edit.get_caret_line()) + +func set_line(position:int): + if !is_mode_visual(mode): + code_edit.set_caret_line(min(position, code_edit.get_line_count()-1)) + return + + selection_to = Vector2i( clampi(selection_to.x, 0, get_line_length(position)), clampi(position, 0, code_edit.get_line_count()) ) + update_visual_selection() + + +func move_column(offset: int): + set_column(get_column()+offset) + +func get_column(): + if is_mode_visual(mode): + return selection_to.x + return code_edit.get_caret_column() + +func set_column(position: int): + if !is_mode_visual(mode): + var line: String = code_edit.get_line(code_edit.get_caret_line()) + code_edit.set_caret_column(min(line.length(), position)) + return + + selection_to = Vector2i( clampi(position, 0, get_line_length(selection_to.y)), clampi(selection_to.y, 0, code_edit.get_line_count()) ) + update_visual_selection() + +func update_visual_selection(): + if mode == Mode.VISUAL: + var to_right: bool = selection_to.x >= selection_from.x or selection_to.y > selection_from.y + code_edit.select( selection_from.y, selection_from.x + int(!to_right), selection_to.y, selection_to.x + int(to_right) ) + elif mode == Mode.VISUAL_LINE: + var f: int = mini(selection_from.y, selection_to.y) - 1 + var t: int = maxi(selection_from.y, selection_to.y) + code_edit.select(f, get_line_length(f), t, get_line_length(t)) + +func is_mode_visual(m: int) -> bool: + return m == Mode.VISUAL or m == Mode.VISUAL_LINE + +func is_lowercase(text: String) -> bool: + return text == text.to_lower() + +func is_uppercase(text: String) -> bool: + return text == text.to_upper() + +func get_stream_char(stream: String, idx: int) -> String: + return stream[idx] if stream.length() > idx else '' + +func draw_cursor(): + if code_edit.is_dragging_cursor(): + selection_from = Vector2i(code_edit.get_selection_from_column(), code_edit.get_selection_from_line()) + selection_to = Vector2i(code_edit.get_selection_to_column(), code_edit.get_selection_to_line()) + + if code_edit.get_selected_text(0).length() > 1 and !is_mode_visual(mode): + code_edit.release_focus() + self.grab_focus() + set_mode(Mode.VISUAL) + + if mode == Mode.INSERT: + if code_edit.has_selection(0): + code_edit.deselect(0) + return + + if mode != Mode.NORMAL: + return + + var line: int = code_edit.get_caret_line() + var column: int = code_edit.get_caret_column() + if column >= code_edit.get_line(line).length(): + column -= 1 + code_edit.set_caret_column(column) + + code_edit.select(line, column, line, column+1) diff --git a/langtons-ant/addons/godot_vim/cursor.gd.uid b/langtons-ant/addons/godot_vim/cursor.gd.uid new file mode 100644 index 0000000..4e346ea --- /dev/null +++ b/langtons-ant/addons/godot_vim/cursor.gd.uid @@ -0,0 +1 @@ +uid://bmkia3drpjivh diff --git a/langtons-ant/addons/godot_vim/dispatcher.gd b/langtons-ant/addons/godot_vim/dispatcher.gd new file mode 100644 index 0000000..f099971 --- /dev/null +++ b/langtons-ant/addons/godot_vim/dispatcher.gd @@ -0,0 +1,22 @@ +extends Object + +var handlers = { + "goto": preload("res://addons/godot_vim/commands/goto.gd"), + "find": preload("res://addons/godot_vim/commands/find.gd"), + "marks": preload("res://addons/godot_vim/commands/marks.gd") +} + +var globals + +func dispatch(command : String): + var command_idx_end = command.find(' ', 1) + if command_idx_end == -1: command_idx_end = command.length() + var handler_name = command.substr(1, command_idx_end-1) + if not handlers.has(handler_name): + return ERR_DOES_NOT_EXIST + + var handler = handlers.get(handler_name) + var handler_instance = handler.new() + var args = command.substr(command_idx_end, command.length()) + handler_instance.execute(globals, args) + return OK diff --git a/langtons-ant/addons/godot_vim/dispatcher.gd.uid b/langtons-ant/addons/godot_vim/dispatcher.gd.uid new file mode 100644 index 0000000..6b36fcf --- /dev/null +++ b/langtons-ant/addons/godot_vim/dispatcher.gd.uid @@ -0,0 +1 @@ +uid://dvfyfj3wtqfds diff --git a/langtons-ant/addons/godot_vim/hack_regular.ttf b/langtons-ant/addons/godot_vim/hack_regular.ttf new file mode 100644 index 0000000..92a90cb Binary files /dev/null and b/langtons-ant/addons/godot_vim/hack_regular.ttf differ diff --git a/langtons-ant/addons/godot_vim/plugin.cfg b/langtons-ant/addons/godot_vim/plugin.cfg new file mode 100644 index 0000000..5726e85 --- /dev/null +++ b/langtons-ant/addons/godot_vim/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GodotVim" +description="" +author="Bernardo Bruning" +version="0.1" +script="plugin.gd" diff --git a/langtons-ant/addons/godot_vim/plugin.gd b/langtons-ant/addons/godot_vim/plugin.gd new file mode 100644 index 0000000..35d10fa --- /dev/null +++ b/langtons-ant/addons/godot_vim/plugin.gd @@ -0,0 +1,164 @@ +@tool +extends EditorPlugin + +enum Mode { NORMAL, INSERT, VISUAL, VISUAL_LINE, COMMAND } + +# Used for commands like "w" "b" and "e" respectively +enum WordEdgeMode { WORD, BEGINNING, END } + +const SPACES: String = " \t" +const KEYWORDS: String = ".,\"'-=+!@#$%^&*()[]{}?~/\\<>:;" +const DIGITS: String = "0123456789" +const StatusBar = preload("res://addons/godot_vim/status_bar.gd") +const CommandLine = preload("res://addons/godot_vim/command_line.gd") +const Cursor = preload("res://addons/godot_vim/cursor.gd") + +var cursor: Cursor +var command_line: CommandLine +var status_bar: StatusBar +var globals: Dictionary = {} + +func _enter_tree(): + globals = {} + + if get_code_edit() != null: + _load() + get_editor_interface().get_script_editor().connect("editor_script_changed", _script_changed) + +func _script_changed(script: Script): + # Add to recent files + var path: String = script.resource_path + var marks: Dictionary = globals.get('marks', {}) + for i in range(9, -1, -1): + var m: String = str(i) + var pm: String = str(i - 1) + if !marks.has(pm): + continue + marks[m] = marks[pm] + marks['-1'] = { 'file' : path, 'pos' : Vector2i(-1, 0) } + + _load() + + +func edit_script(path: String, pos: Vector2i): + var script = load(path) + var editor_interface: EditorInterface = globals.editor_interface + if script == null: + status_bar.display_error('Could not open file "%s"' % path) + return '' + editor_interface.edit_script(script) + cursor.call_deferred('set_caret_pos', pos.y, pos.x) + + +func _load(): + if globals == null: + globals = {} + + # Cursor + if cursor != null: + cursor.queue_free() + cursor = Cursor.new() + var code_edit = get_code_edit() + code_edit.select(code_edit.get_caret_line(), code_edit.get_caret_column(), code_edit.get_caret_line(), code_edit.get_caret_column()+1) + cursor.code_edit = code_edit + cursor.globals = globals + + # Command line + if command_line != null: + command_line.queue_free() + command_line = CommandLine.new() + command_line.code_edit = code_edit + cursor.command_line = command_line + command_line.cursor = cursor + command_line.globals = globals + command_line.hide() + + # Status bar + if status_bar != null: + status_bar.queue_free() + status_bar = StatusBar.new() + cursor.status_bar = status_bar + command_line.status_bar = status_bar + + var editor_interface = get_editor_interface() + if editor_interface == null: return + var script_editor = editor_interface.get_script_editor() + if script_editor == null: return + var script_editor_base = script_editor.get_current_editor() + if script_editor_base == null: return + + globals.editor_interface = editor_interface + globals.command_line = command_line + globals.status_bar = status_bar + globals.code_edit = code_edit + globals.cursor = cursor + globals.script_editor = script_editor + globals.vim_plugin = self + script_editor_base.add_child(cursor) + script_editor_base.add_child(status_bar) + script_editor_base.add_child(command_line) + + +func get_code_edit(): + var editor = get_editor_interface().get_script_editor().get_current_editor() + return _select(editor, ['VSplitContainer', 'CodeTextEditor', 'CodeEdit']) + +func _select(obj: Node, types: Array[String]): # ??? + for type in types: + for child in obj.get_children(): + if child.is_class(type): + obj = child + continue + return obj + +func _exit_tree(): + if cursor != null: + cursor.queue_free() + if command_line != null: + command_line.queue_free() + if status_bar != null: + status_bar.queue_free() + + +# ------------------------------------------------------------- +# ** UTIL ** +# ------------------------------------------------------------- + +func search_regex(text_edit: TextEdit, pattern: String, from_pos: Vector2i) -> RegExMatch: + var regex: RegEx = RegEx.new() + var err: int = regex.compile(pattern) + var idx: int = pos_to_idx(text_edit, from_pos) + var res: RegExMatch = regex.search(text_edit.text, idx) + if res == null: + return regex.search(text_edit.text, 0) + return res + +func search_regex_backwards(text_edit: TextEdit, pattern: String, from_pos: Vector2i) -> RegExMatch: + var regex: RegEx = RegEx.new() + var err: int = regex.compile(pattern) + var idx: int = pos_to_idx(text_edit, from_pos) + # We use pop_back() so it doesn't print an error + var res: RegExMatch = regex.search_all(text_edit.text, 0, idx).pop_back() + if res == null: + return regex.search_all(text_edit.text).pop_back() + return res + +func pos_to_idx(text_edit: TextEdit, pos: Vector2i) -> int: + text_edit.select(0, 0, pos.y, pos.x) + var len: int = text_edit.get_selected_text().length() + text_edit.deselect() + return len + +func idx_to_pos(text_edit: TextEdit, idx: int) -> Vector2i: + var line: int = text_edit.text .count('\n', 0, idx) + var col: int = idx - text_edit.text .rfind('\n', idx) - 1 + return Vector2i(col, line) + +func get_first_non_digit_idx(str: String) -> int: + if str.is_empty(): return -1 + if str[0] == '0': return 0 # '0...' is an exception + for i in str.length(): + if !DIGITS.contains(str[i]): + return i + return -1 # All digits + diff --git a/langtons-ant/addons/godot_vim/plugin.gd.uid b/langtons-ant/addons/godot_vim/plugin.gd.uid new file mode 100644 index 0000000..fad9375 --- /dev/null +++ b/langtons-ant/addons/godot_vim/plugin.gd.uid @@ -0,0 +1 @@ +uid://duaf0qcswso06 diff --git a/langtons-ant/addons/godot_vim/status_bar.gd b/langtons-ant/addons/godot_vim/status_bar.gd new file mode 100644 index 0000000..7160c14 --- /dev/null +++ b/langtons-ant/addons/godot_vim/status_bar.gd @@ -0,0 +1,68 @@ +extends HBoxContainer +const ERROR_COLOR: String = "#ff8866" +const SPECIAL_COLOR: String = "#fcba03" + +const Constants = preload("res://addons/godot_vim/constants.gd") +const Mode = Constants.Mode + +var mode_label: Label +var main_label: RichTextLabel + +func _ready(): + var font = load("res://addons/godot_vim/hack_regular.ttf") + + mode_label = Label.new() + + mode_label.text = '' + mode_label.add_theme_color_override(&"font_color", Color.BLACK) + var stylebox: StyleBoxFlat = StyleBoxFlat.new() + stylebox.bg_color = Color.GOLD + stylebox.content_margin_left = 4.0 + stylebox.content_margin_right = 4.0 + stylebox.content_margin_top = 2.0 + stylebox.content_margin_bottom = 2.0 + mode_label.add_theme_stylebox_override(&"normal", stylebox) + mode_label.add_theme_font_override(&"font", font) + add_child(mode_label) + + main_label = RichTextLabel.new() + main_label.bbcode_enabled = true + main_label.text = '' + main_label.fit_content = true + main_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + main_label.text_direction = Control.TEXT_DIRECTION_RTL + main_label.add_theme_font_override(&"normal_font", font) + add_child(main_label) + +func display_text(text: String, text_direction: Control.TextDirection = TEXT_DIRECTION_RTL): + main_label.text = text + main_label.text_direction = text_direction + +func display_error(text: String): + main_label.text = '[color=%s]%s' % [ERROR_COLOR, text] + main_label.text_direction = Control.TEXT_DIRECTION_LTR + +func display_special(text: String): + main_label.text = '[color=%s]%s' % [SPECIAL_COLOR, text] + main_label.text_direction = Control.TEXT_DIRECTION_LTR + +func set_mode_text(mode: Mode): + var stylebox: StyleBoxFlat = mode_label.get_theme_stylebox(&"normal") + match mode: + Mode.NORMAL: + mode_label.text = 'NORMAL' + stylebox.bg_color = Color.LIGHT_SALMON + Mode.INSERT: + mode_label.text = 'INSERT' + stylebox.bg_color = Color.POWDER_BLUE + Mode.VISUAL: + mode_label.text = 'VISUAL' + stylebox.bg_color = Color.PLUM + Mode.VISUAL_LINE: + mode_label.text = 'VISUAL LINE' + stylebox.bg_color = Color.PLUM + Mode.COMMAND: + mode_label.text = 'COMMAND' + stylebox.bg_color = Color.TOMATO + _: + pass diff --git a/langtons-ant/addons/godot_vim/status_bar.gd.uid b/langtons-ant/addons/godot_vim/status_bar.gd.uid new file mode 100644 index 0000000..1d9aeb6 --- /dev/null +++ b/langtons-ant/addons/godot_vim/status_bar.gd.uid @@ -0,0 +1 @@ +uid://bsixy362mx4r7 diff --git a/langtons-ant/ant.gd b/langtons-ant/ant.gd new file mode 100644 index 0000000..904610c --- /dev/null +++ b/langtons-ant/ant.gd @@ -0,0 +1,60 @@ +extends Sprite2D + +var pos: Vector2i = Vector2i(0,0) +var dir: TileSet.CellNeighbor = TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE +@onready var tiles: TileMapLayer = get_node("TileMapLayer") + +var next_dir: Dictionary = { + 0: TileSet.CELL_NEIGHBOR_RIGHT_SIDE, + 1: TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE, + 2: TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE, + 3: TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE, + 4: TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE, +} + +var next_int: Dictionary = { + TileSet.CELL_NEIGHBOR_RIGHT_SIDE: 1, + TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE: 2, + TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE: 3, + TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE: 4, + TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE: 0, +} + +func _ready() -> void: + set_frame(1) + +func update() -> void: + var tile: int = tiles.get_tile_colour(pos) + print("tile %s" % tile) + var newFrame: int = next_int[dir] + + if tile == 0: + newFrame = (newFrame + 1) % 5 + dir = next_dir[newFrame] + else: + newFrame = (newFrame) % 5 + dir = next_dir[newFrame] + + #var tmpPos: Vector2i + #for i in range(0, 16): + #tmpPos = tiles.get_neighbor_cell(pos, i) + #print("%s: (%d %d)" % [i, tmpPos.x, tmpPos.y]) + + print("(%d %d)" % [pos.x, pos.y]) + pos = tiles.get_neighbor_cell(pos, dir) + tile = tiles.get_tile_colour(pos) + print("(%d %d)" % [pos.x, pos.y]) + tiles.set_tile(pos, (tile + 1) % 2) # TODO: should probably use another dictionary + set_frame(newFrame) + +func _physics_process(delta: float) -> void: + var vel: Vector2 = Vector2(0,0) + match dir: + TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE: + vel = Vector2(1,1) + TileSet.CELL_NEIGHBOR_RIGHT_SIDE: + vel = Vector2(1,0) + _: + vel = Vector2(0,-1) + vel = vel.normalized() * 16 + global_position += vel * delta diff --git a/langtons-ant/ant.gd.uid b/langtons-ant/ant.gd.uid new file mode 100644 index 0000000..c00e886 --- /dev/null +++ b/langtons-ant/ant.gd.uid @@ -0,0 +1 @@ +uid://b6ll30b7xtwal diff --git a/langtons-ant/assets/ant.png b/langtons-ant/assets/ant.png new file mode 100644 index 0000000..e4d3132 Binary files /dev/null and b/langtons-ant/assets/ant.png differ diff --git a/langtons-ant/assets/tile0.png b/langtons-ant/assets/tile0.png new file mode 100644 index 0000000..27959af Binary files /dev/null and b/langtons-ant/assets/tile0.png differ diff --git a/langtons-ant/assets/tile1.png b/langtons-ant/assets/tile1.png new file mode 100644 index 0000000..a17a638 Binary files /dev/null and b/langtons-ant/assets/tile1.png differ diff --git a/langtons-ant/assets/tiles.png b/langtons-ant/assets/tiles.png new file mode 100644 index 0000000..9c378e5 Binary files /dev/null and b/langtons-ant/assets/tiles.png differ diff --git a/langtons-ant/grid.gd b/langtons-ant/grid.gd new file mode 100644 index 0000000..60c7912 --- /dev/null +++ b/langtons-ant/grid.gd @@ -0,0 +1,18 @@ +extends Node2D + +var t: float = 0 +@export var speed: float = 58 +@export var ant: Sprite2D +@export var tiles: TileMapLayer + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta: float) -> void: + t += delta + if (t > 60-speed): + t = 0 + ant.update() diff --git a/langtons-ant/grid.gd.uid b/langtons-ant/grid.gd.uid new file mode 100644 index 0000000..33251a6 --- /dev/null +++ b/langtons-ant/grid.gd.uid @@ -0,0 +1 @@ +uid://by3d3t5ymxl3m diff --git a/langtons-ant/grid.tscn b/langtons-ant/grid.tscn new file mode 100644 index 0000000..f6069f5 --- /dev/null +++ b/langtons-ant/grid.tscn @@ -0,0 +1,43 @@ +[gd_scene load_steps=8 format=3 uid="uid://b6xi32r3co6md"] + +[ext_resource type="Script" uid="uid://by3d3t5ymxl3m" path="res://grid.gd" id="1_bghhw"] +[ext_resource type="Script" uid="uid://b6ll30b7xtwal" path="res://ant.gd" id="1_ebq2e"] +[ext_resource type="Texture2D" uid="uid://bnfy5vx72ux33" path="res://assets/ant.png" id="3_sle3t"] +[ext_resource type="Texture2D" uid="uid://d0v1qlkltasln" path="res://assets/tiles.png" id="4_fqc2p"] +[ext_resource type="Script" uid="uid://cqxfmo3hixad1" path="res://tile_map_layer.gd" id="5_fqc2p"] + +[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_g2qvd"] +texture = ExtResource("4_fqc2p") +texture_region_size = Vector2i(32, 32) +0:0/0 = 0 +1:0/0 = 0 +0:1/0 = 0 +1:1/0 = 0 +0:2/0 = 0 +1:2/0 = 0 +0:3/0 = 0 +1:3/0 = 0 + +[sub_resource type="TileSet" id="TileSet_05i0m"] +tile_shape = 3 +tile_size = Vector2i(32, 32) +sources/0 = SubResource("TileSetAtlasSource_g2qvd") + +[node name="Grid" type="Node2D" node_paths=PackedStringArray("ant", "tiles")] +script = ExtResource("1_bghhw") +speed = 60.0 +ant = NodePath("ant") +tiles = NodePath("ant/TileMapLayer") + +[node name="ant" type="Sprite2D" parent="."] +texture = ExtResource("3_sle3t") +vframes = 6 +script = ExtResource("1_ebq2e") + +[node name="Camera2D" type="Camera2D" parent="ant"] + +[node name="TileMapLayer" type="TileMapLayer" parent="ant"] +position = Vector2(-74, -8) +tile_set = SubResource("TileSet_05i0m") +collision_enabled = false +script = ExtResource("5_fqc2p") diff --git a/langtons-ant/icon.svg b/langtons-ant/icon.svg new file mode 100644 index 0000000..c6bbb7d --- /dev/null +++ b/langtons-ant/icon.svg @@ -0,0 +1 @@ + diff --git a/langtons-ant/tile_map_layer.gd b/langtons-ant/tile_map_layer.gd new file mode 100644 index 0000000..e3b9dfc --- /dev/null +++ b/langtons-ant/tile_map_layer.gd @@ -0,0 +1,61 @@ +extends TileMapLayer + +var tiles: Dictionary = {} + +func get_tile_colour(pos: Vector2i) -> int: + return tiles.get_or_add(pos, 0) + +# TODO: state sh1ould be an enum probably +func set_tile(pos: Vector2i, state: int) -> void: + print("setting cell (%s) at %d %d\n" % [state, pos.x, pos.y]) + set_cell(pos, 0, Vector2i(0, state)) + tiles.erase(pos) + tiles.get_or_add(pos, state) + +#var direction = 0 +#var antPos = Vector2i(0,0) +#var grid = {} +#var tileSize = 16 +# +## Called when the node enters the scene tree for the first time. +#func _ready() -> void: + #set_process(true) + #grid[antPos] = 0 + #updateTile(antPos, 0) +# +#func updateTile(pos: Vector2i, col: int): + #var tileCol = Color.WHITE if col == 0 else Color.BLACK + #var tileRect = ColorRect.new() + #tileRect.color = tileCol + #tileRect.size = Vector2(tileSize, tileSize) + #tileRect.position = Vector2(pos.x * tileSize, pos.y * tileSize) + #add_child(tileRect) + # +# +## Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta: float) -> void: + #moveAnt() +# +#func moveAnt(): + #var curCol = grid.get(antPos, 0) + # + #if curCol == 0: + #direction = (direction + 1) % 4 + #else: + #direction = (direction + 3) % 4 + # + #grid[antPos] = 1 - curCol + #updateTile(antPos, grid[antPos]) + # + #match direction: + #0: + #antPos.y -= 1 + #1: + #antPos.x += 1 + #2: + #antPos.y += 1 + #3: + #antPos.x -= 1 + # + #if not grid.has(antPos): + #grid[antPos] = 0 diff --git a/langtons-ant/tile_map_layer.gd.uid b/langtons-ant/tile_map_layer.gd.uid new file mode 100644 index 0000000..269978d --- /dev/null +++ b/langtons-ant/tile_map_layer.gd.uid @@ -0,0 +1 @@ +uid://cqxfmo3hixad1