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 @@
+
+
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