├── .gitattributes ├── .gitignore ├── LICENSE.md ├── README.md └── addons └── godot-vim ├── godot-vim.gd ├── icon.svg └── plugin.cfg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | project.godot 4 | *.import 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Joshua Najera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VIM emulator for Godot 4 (version 4.3.0) 2 | 3 | ## What is this? 4 | This is a Godot 4 plugin to emulates VIM-like editor behavior witin Godot editor itself (i.e. after installed and enabled, one could edit (kind of) like in VIM. 5 | 6 | ## How to install? 7 | 1. Clone or download the source of the repo, and copy the `addons` folder to the root of your project. 8 | 2. Go to `Project` -> `Project Settings` -> `Plugins` tab and check the check box of the plugin named `godot-vim` 9 | 10 | ## What's new? 11 | - Changed version schema to godot_major_version.godot_minir_version.vim_plugin_version, so that it is easier to find the correct version of plugin for specific version of Godot editors 12 | - Created a new branch `4.2` for Godot Editors 4.0 to 4.2 13 | - Fixed selection issues due to CodeEdit.select() behavior change 14 | 15 | 16 | ## VIM features supprted 17 | #### Mode 18 | 19 | - Normal mode 20 | - Insert mode 21 | - Visual mode 22 | - Visual line mode 23 | 24 | #### Motions 25 | 26 | h, l, j, k, +, - 27 | ^, 0, $, | 28 | H, L, M, 29 | c-f, c-b, c-d, c-u, 30 | G, gg 31 | w, W, e, E, b, ge 32 | %, f, F, t, T, ; 33 | *, #, /, n, N 34 | aw, a(, a{, a[, a", a' 35 | iw, i(, i{, i[, i", i' 36 | {, } 37 | 38 | #### Operator 39 | 40 | c, C, 41 | d, D, x, X, 42 | y, Y, 43 | u, U, ~ 44 | 45 | #### Actions 46 | 47 | p, 48 | u, c-r, 49 | c-o, c-i, 50 | za, zM, zR, 51 | q, @, ., 52 | >, < 53 | m, ' 54 | 55 | ## FAQ 56 | 1. How to override default Godot shortcuts with `godot-vim`'s ones 57 | 58 | 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`. 59 | 60 | This currently goes for: 61 | 62 | - `Ctrl+R` 63 | - `Ctrl+U` 64 | - `Ctrl+D` 65 | 66 | 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/wenqiangwang/godot-vim/blob/main/addons/godot-vim/godot-vim.gd#L138 67 | 68 | 2. I found a problem, what should I do? 69 | 70 | - You could debug by yourself, changing the value of `DEBUGGING` variable to 1 to see logs 71 | - Or report the issue by providing a) the content of original text editing, b) the cursor position and c) key sequence that repros the issue 72 | -------------------------------------------------------------------------------- /addons/godot-vim/godot-vim.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | const INF_COL : int = 99999 6 | const DEBUGGING : int = 0 # Change to 1 for debugging 7 | const CODE_MACRO_PLAY_END : int = 10000 8 | 9 | 10 | 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 } 11 | const WHITESPACE: Dictionary = { ' ': 1, ' ': 1, '\n' : 1 } 12 | 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 } 13 | 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 } 14 | const SYMBOLS = { "(": ")", ")": "(", "[": "]", "]": "[", "{": "}", "}": "{", "<": ">", ">": "<", '"': '"', "'": "'" } 15 | 16 | 17 | enum { 18 | MOTION, 19 | OPERATOR, 20 | OPERATOR_MOTION, 21 | ACTION, 22 | } 23 | 24 | 25 | enum Context { 26 | NORMAL, 27 | VISUAL, 28 | } 29 | 30 | 31 | var the_key_map : Array[Dictionary] = [ 32 | # Move 33 | { "keys": ["H"], "type": MOTION, "motion": "move_by_characters", "motion_args": { "forward": false } }, 34 | { "keys": ["L"], "type": MOTION, "motion": "move_by_characters", "motion_args": { "forward": true } }, 35 | { "keys": ["J"], "type": MOTION, "motion": "move_by_lines", "motion_args": { "forward": true, "line_wise": true } }, 36 | { "keys": ["K"], "type": MOTION, "motion": "move_by_lines", "motion_args": { "forward": false, "line_wise": true } }, 37 | { "keys": ["Shift+Equal"], "type": MOTION, "motion": "move_by_lines", "motion_args": { "forward": true, "to_first_char": true } }, 38 | { "keys": ["Minus"], "type": MOTION, "motion": "move_by_lines", "motion_args": { "forward": false, "to_first_char": true } }, 39 | { "keys": ["Shift+4"], "type": MOTION, "motion": "move_to_end_of_line", "motion_args": { "inclusive": true } }, 40 | { "keys": ["Shift+6"], "type": MOTION, "motion": "move_to_first_non_white_space_character" }, 41 | { "keys": ["0"], "type": MOTION, "motion": "move_to_start_of_line" }, 42 | { "keys": ["Shift+H"], "type": MOTION, "motion": "move_to_top_line", "motion_args": { "to_jump_list": true } }, 43 | { "keys": ["Shift+L"], "type": MOTION, "motion": "move_to_bottom_line", "motion_args": { "to_jump_list": true } }, 44 | { "keys": ["Shift+M"], "type": MOTION, "motion": "move_to_middle_line", "motion_args": { "to_jump_list": true } }, 45 | { "keys": ["G", "G"], "type": MOTION, "motion": "move_to_line_or_edge_of_document", "motion_args": { "forward": false, "to_jump_list": true } }, 46 | { "keys": ["Shift+G"], "type": MOTION, "motion": "move_to_line_or_edge_of_document", "motion_args": { "forward": true, "to_jump_list": true } }, 47 | { "keys": ["Ctrl+F"], "type": MOTION, "motion": "move_by_page", "motion_args": { "forward": true } }, 48 | { "keys": ["Ctrl+B"], "type": MOTION, "motion": "move_by_page", "motion_args": { "forward": false } }, 49 | { "keys": ["Ctrl+D"], "type": MOTION, "motion": "move_by_scroll", "motion_args": { "forward": true } }, 50 | { "keys": ["Ctrl+U"], "type": MOTION, "motion": "move_by_scroll", "motion_args": { "forward": false } }, 51 | { "keys": ["Shift+BackSlash"], "type": MOTION, "motion": "move_to_column" }, 52 | { "keys": ["W"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": true, "word_end": false, "big_word": false } }, 53 | { "keys": ["Shift+W"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": true, "word_end": false, "big_word": true } }, 54 | { "keys": ["E"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": true, "word_end": true, "big_word": false, "inclusive": true } }, 55 | { "keys": ["Shift+E"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": true, "word_end": true, "big_word": true, "inclusive": true } }, 56 | { "keys": ["B"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": false, "word_end": false, "big_word": false } }, 57 | { "keys": ["Shift+B"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": false, "word_end": false, "big_word": true } }, 58 | { "keys": ["G", "E"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": false, "word_end": true, "big_word": false } }, 59 | { "keys": ["G", "Shift+E"], "type": MOTION, "motion": "move_by_words", "motion_args": { "forward": false, "word_end": true, "big_word": true } }, 60 | { "keys": ["Shift+5"], "type": MOTION, "motion": "move_to_matched_symbol", "motion_args": { "inclusive": true, "to_jump_list": true } }, 61 | { "keys": ["F", "{char}"], "type": MOTION, "motion": "move_to_next_char", "motion_args": { "forward": true, "inclusive": true } }, 62 | { "keys": ["Shift+F", "{char}"], "type": MOTION, "motion": "move_to_next_char", "motion_args": { "forward": false } }, 63 | { "keys": ["T", "{char}"], "type": MOTION, "motion": "move_to_next_char", "motion_args": { "forward": true, "stop_before": true, "inclusive": true } }, 64 | { "keys": ["Shift+T", "{char}"], "type": MOTION, "motion": "move_to_next_char", "motion_args": { "forward": false, "stop_before": true } }, 65 | { "keys": ["Shift+BracketRight"], "type": MOTION, "motion": "move_by_paragraph", "motion_args": { "forward": true } }, 66 | { "keys": ["Shift+BracketLeft"], "type": MOTION, "motion": "move_by_paragraph", "motion_args": { "forward": false } }, 67 | { "keys": ["Semicolon"], "type": MOTION, "motion": "repeat_last_char_search", "motion_args": {} }, 68 | { "keys": ["Shift+8"], "type": MOTION, "motion": "find_word_under_caret", "motion_args": { "forward": true, "to_jump_list": true } }, 69 | { "keys": ["Shift+3"], "type": MOTION, "motion": "find_word_under_caret", "motion_args": { "forward": false, "to_jump_list": true } }, 70 | { "keys": ["N"], "type": MOTION, "motion": "find_again", "motion_args": { "forward": true, "to_jump_list": true } }, 71 | { "keys": ["Shift+N"], "type": MOTION, "motion": "find_again", "motion_args": { "forward": false, "to_jump_list": true } }, 72 | { "keys": ["A", "Shift+9"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"(" } }, 73 | { "keys": ["A", "Shift+0"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"(" } }, 74 | { "keys": ["A", "B"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"(" } }, 75 | { "keys": ["A", "BracketLeft"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"[" } }, 76 | { "keys": ["A", "BracketRight"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"[" } }, 77 | { "keys": ["A", "Shift+BracketLeft"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"{" } }, 78 | { "keys": ["A", "Shift+BracketRight"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"{" } }, 79 | { "keys": ["A", "Shift+B"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"{" } }, 80 | { "keys": ["A", "Apostrophe"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":"'" } }, 81 | { "keys": ["A", 'Shift+Apostrophe'], "type": MOTION, "motion": "text_object", "motion_args": { "inner": false, "object":'"' } }, 82 | { "keys": ["I", "Shift+9"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"(" } }, 83 | { "keys": ["I", "Shift+0"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"(" } }, 84 | { "keys": ["I", "B"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"(" } }, 85 | { "keys": ["I", "BracketLeft"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"[" } }, 86 | { "keys": ["I", "BracketRight"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"[" } }, 87 | { "keys": ["I", "Shift+BracketLeft"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"{" } }, 88 | { "keys": ["I", "Shift+BracketRight"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"{" } }, 89 | { "keys": ["I", "Shift+B"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"{" } }, 90 | { "keys": ["I", "Apostrophe"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"'" } }, 91 | { "keys": ["I", 'Shift+Apostrophe'], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":'"' } }, 92 | { "keys": ["I", "W"], "type": MOTION, "motion": "text_object", "motion_args": { "inner": true, "object":"w" } }, 93 | { "keys": ["Apostrophe", "{char}"], "type": MOTION, "motion": "go_to_bookmark", "motion_args": {} }, 94 | { "keys": ["D"], "type": OPERATOR, "operator": "delete" }, 95 | { "keys": ["Shift+D"], "type": OPERATOR_MOTION, "operator": "delete", "motion": "move_to_end_of_line", "motion_args": { "inclusive": true } }, 96 | { "keys": ["Y"], "type": OPERATOR, "operator": "yank", "operator_args": { "maintain_position": true } }, 97 | { "keys": ["Shift+Y"], "type": OPERATOR_MOTION, "operator": "yank", "motion": "move_to_end_of_line", "motion_args": { "inclusive": true }, "operator_args": { "maintain_position": true } }, 98 | { "keys": ["C"], "type": OPERATOR, "operator": "change" }, 99 | { "keys": ["Shift+C"], "type": OPERATOR_MOTION, "operator": "change", "motion": "move_to_end_of_line", "motion_args": { "inclusive": true } }, 100 | { "keys": ["X"], "type": OPERATOR_MOTION, "operator": "delete", "motion": "move_by_characters", "motion_args": { "forward": true, "one_line": true }, "context": Context.NORMAL }, 101 | { "keys": ["S"], "type": OPERATOR_MOTION, "operator": "change", "motion": "move_by_characters", "motion_args": { "forward": true }, "context": Context.NORMAL }, 102 | { "keys": ["X"], "type": OPERATOR, "operator": "delete", "context": Context.VISUAL }, 103 | { "keys": ["Shift+X"], "type": OPERATOR_MOTION, "operator": "delete", "motion": "move_by_characters", "motion_args": { "forward": false } }, 104 | { "keys": ["G", "U"], "type": OPERATOR, "operator": "change_case", "operator_args": { "lower": true }, "context": Context.VISUAL }, 105 | { "keys": ["G", "Shift+U"], "type": OPERATOR, "operator": "change_case", "operator_args": { "lower": false }, "context": Context.VISUAL }, 106 | { "keys": ["U"], "type": OPERATOR, "operator": "change_case", "operator_args": { "lower": true }, "context": Context.VISUAL }, 107 | { "keys": ["Shift+U"], "type": OPERATOR, "operator": "change_case", "operator_args": { "lower": false }, "context": Context.VISUAL }, 108 | { "keys": ["Shift+QuoteLeft"], "type": OPERATOR, "operator": "toggle_case", "operator_args": {}, "context": Context.VISUAL }, 109 | { "keys": ["Shift+QuoteLeft"], "type": OPERATOR_MOTION, "operator": "toggle_case", "motion": "move_by_characters", "motion_args": { "forward": true }, "context": Context.NORMAL }, 110 | { "keys": ["P"], "type": ACTION, "action": "paste", "action_args": { "after": true } }, 111 | { "keys": ["Shift+P"], "type": ACTION, "action": "paste", "action_args": { "after": false } }, 112 | { "keys": ["U"], "type": ACTION, "action": "undo", "action_args": {}, "context": Context.NORMAL }, 113 | { "keys": ["Ctrl+R"], "type": ACTION, "action": "redo", "action_args": {} }, 114 | { "keys": ["R", "{char}"], "type": ACTION, "action": "replace", "action_args": {} }, 115 | { "keys": ["Period"], "type": ACTION, "action": "repeat_last_edit", "action_args": {} }, 116 | { "keys": ["I"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "inplace" }, "context": Context.NORMAL }, 117 | { "keys": ["Shift+I"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "bol" } }, 118 | { "keys": ["A"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "after" }, "context": Context.NORMAL }, 119 | { "keys": ["Shift+A"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "eol" } }, 120 | { "keys": ["O"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "new_line_below" } }, 121 | { "keys": ["Shift+O"], "type": ACTION, "action": "enter_insert_mode", "action_args": { "insert_at": "new_line_above" } }, 122 | { "keys": ["V"], "type": ACTION, "action": "enter_visual_mode", "action_args": { "line_wise": false } }, 123 | { "keys": ["Shift+V"], "type": ACTION, "action": "enter_visual_mode", "action_args": { "line_wise": true } }, 124 | { "keys": ["Slash"], "type": ACTION, "action": "search", "action_args": {} }, 125 | { "keys": ["Ctrl+O"], "type": ACTION, "action": "jump_list_walk", "action_args": { "forward": false } }, 126 | { "keys": ["Ctrl+I"], "type": ACTION, "action": "jump_list_walk", "action_args": { "forward": true } }, 127 | { "keys": ["Z", "A"], "type": ACTION, "action": "toggle_folding", }, 128 | { "keys": ["Z", "Shift+M"], "type": ACTION, "action": "fold_all", }, 129 | { "keys": ["Z", "Shift+R"], "type": ACTION, "action": "unfold_all", }, 130 | { "keys": ["Q", "{char}"], "type": ACTION, "action": "record_macro", "when_not": "is_recording" }, 131 | { "keys": ["Q"], "type": ACTION, "action": "stop_record_macro", "when": "is_recording" }, 132 | { "keys": ["Shift+2", "{char}"], "type": ACTION, "action": "play_macro", }, 133 | { "keys": ["Shift+Comma"], "type": ACTION, "action": "indent", "action_args": { "forward" = false} }, 134 | { "keys": ["Shift+Period"], "type": ACTION, "action": "indent", "action_args": { "forward" = true } }, 135 | { "keys": ["Shift+J"], "type": ACTION, "action": "join_lines", "action_args": {} }, 136 | { "keys": ["M", "{char}"], "type": ACTION, "action": "set_bookmark", "action_args": {} }, 137 | { "keys": ["Ctrl+BracketRight"], "type": ACTION, "action": "go_to_definition", "action_args": {} }, 138 | ] 139 | 140 | 141 | # The list of command keys we handle (other command keys will be handled by Godot) 142 | var command_keys_white_list : Dictionary = { 143 | "Escape": 1, 144 | "Enter": 1, 145 | # "Ctrl+F": 1, # Uncomment if you would like move-forward by page function instead of search on slash 146 | "Ctrl+B": 1, 147 | "Ctrl+U": 1, 148 | "Ctrl+D": 1, 149 | "Ctrl+O": 1, 150 | "Ctrl+I": 1, 151 | "Ctrl+R": 1, 152 | "Ctrl+BracketRight": 1, 153 | "Ctrl+BracketLeft": 1, 154 | } 155 | 156 | 157 | var editor_interface : EditorInterface 158 | var the_ed := EditorAdaptor.new() # The current editor adaptor 159 | var the_vim := Vim.new() 160 | var the_dispatcher := CommandDispatcher.new(the_key_map) # The command dispatcher 161 | var disabled := false 162 | 163 | 164 | func _enter_tree() -> void: 165 | if FileAccess.file_exists("res://.novim"): 166 | disabled = true 167 | return 168 | 169 | editor_interface = get_editor_interface() 170 | var script_editor = editor_interface.get_script_editor() 171 | script_editor.editor_script_changed.connect(on_script_changed) 172 | script_editor.script_close.connect(on_script_closed) 173 | on_script_changed(script_editor.get_current_script()) 174 | 175 | var settings = editor_interface.get_editor_settings() 176 | settings.settings_changed.connect(on_settings_changed) 177 | on_settings_changed() 178 | 179 | var find_bar = find_first_node_of_type(script_editor, 'FindReplaceBar') 180 | var find_bar_line_edit : LineEdit = find_first_node_of_type(find_bar, 'LineEdit') 181 | find_bar_line_edit.text_changed.connect(on_search_text_changed) 182 | 183 | 184 | func _input(event) -> void: 185 | if disabled: 186 | return 187 | 188 | var key = event as InputEventKey 189 | 190 | # Don't process when not a key action 191 | if key == null or !key.is_pressed() or not the_ed or not the_ed.has_focus(): 192 | return 193 | 194 | if key.get_keycode_with_modifiers() == KEY_NONE and key.unicode == CODE_MACRO_PLAY_END: 195 | the_vim.macro_manager.on_macro_finished(the_ed) 196 | get_viewport().set_input_as_handled() 197 | return 198 | 199 | # Check to not block some reserved keys (we only handle unicode keys and the white list) 200 | var key_code = key.as_text_keycode() 201 | if DEBUGGING: 202 | print("Key: %s Buffer: %s" % [key_code, the_vim.current.input_state.key_codes()]) 203 | 204 | # We only process keys in the white list or it is ASCII char or SHIFT+ASCII char 205 | if key.get_keycode_with_modifiers() & (~KEY_MASK_SHIFT) > 128 and key_code not in command_keys_white_list: 206 | return 207 | 208 | if the_dispatcher.dispatch(key, the_vim, the_ed): 209 | get_viewport().set_input_as_handled() 210 | 211 | 212 | func on_script_changed(s: Script) -> void: 213 | the_vim.set_current_session(s, the_ed) 214 | 215 | var script_editor = editor_interface.get_script_editor() 216 | 217 | var scrpit_editor_base := script_editor.get_current_editor() 218 | if scrpit_editor_base: 219 | var code_editor := scrpit_editor_base.get_base_editor() as CodeEdit 220 | the_ed.set_code_editor(code_editor) 221 | the_ed.set_block_caret(true) 222 | 223 | if not code_editor.is_connected("caret_changed", on_caret_changed): 224 | code_editor.caret_changed.connect(on_caret_changed) 225 | if not code_editor.is_connected("lines_edited_from", on_lines_edited_from): 226 | code_editor.lines_edited_from.connect(on_lines_edited_from) 227 | 228 | 229 | func on_script_closed(s: Script) -> void: 230 | the_vim.remove_session(s) 231 | 232 | 233 | func on_settings_changed() -> void: 234 | var settings := editor_interface.get_editor_settings() 235 | the_ed.notify_settings_changed(settings) 236 | 237 | 238 | func on_caret_changed()-> void: 239 | the_ed.set_block_caret(not the_vim.current.insert_mode) 240 | 241 | 242 | func on_lines_edited_from(from: int, to: int) -> void: 243 | the_vim.current.jump_list.on_lines_edited(from, to) 244 | the_vim.current.text_change_number += 1 245 | the_vim.current.bookmark_manager.on_lines_edited(from, to) 246 | 247 | 248 | func on_search_text_changed(new_search_text: String) -> void: 249 | the_vim.search_buffer = new_search_text 250 | 251 | 252 | static func find_first_node_of_type(p: Node, type: String) -> Node: 253 | if p.get_class() == type: 254 | return p 255 | for c in p.get_children(): 256 | var t := find_first_node_of_type(c, type) 257 | if t: 258 | return t 259 | return null 260 | 261 | 262 | class Command: 263 | 264 | ### MOTIONS 265 | 266 | static func move_by_characters(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 267 | var one_line = args.get('one_line', false) 268 | var col : int = cur.column + args.repeat * (1 if args.forward else -1) 269 | var line := cur.line 270 | if col > ed.last_column(line): 271 | if one_line: 272 | col = ed.last_column(line) + 1 273 | else: 274 | line += 1 275 | col = 0 276 | elif col < 0: 277 | if one_line: 278 | col = 0 279 | else: 280 | line -= 1 281 | col = ed.last_column(line) 282 | 283 | return Position.new(line, col) 284 | 285 | static func move_by_scroll(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 286 | var count = ed.get_visible_line_count(ed.first_visible_line(), ed.last_visible_line()) 287 | return Position.new(ed.next_unfolded_line(cur.line, count / 2, args.forward), cur.column) 288 | 289 | static func move_by_page(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 290 | var count = ed.get_visible_line_count(ed.first_visible_line(), ed.last_visible_line()) 291 | return Position.new(ed.next_unfolded_line(cur.line, count, args.forward), cur.column) 292 | 293 | static func move_to_column(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 294 | return Position.new(cur.line, args.repeat - 1) 295 | 296 | static func move_by_lines(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 297 | # Depending what our last motion was, we may want to do different things. 298 | # If our last motion was moving vertically, we want to preserve the column from our 299 | # last horizontal move. If our last motion was going to the end of a line, 300 | # moving vertically we should go to the end of the line, etc. 301 | var col : int = cur.column 302 | match vim.current.last_motion: 303 | "move_by_lines", "move_by_scroll", "move_by_page", "move_to_end_of_line", "move_to_column": 304 | col = vim.current.last_h_pos 305 | _: 306 | vim.current.last_h_pos = col 307 | 308 | var line = ed.next_unfolded_line(cur.line, args.repeat, args.forward) 309 | 310 | if args.get("to_first_char", false): 311 | col = ed.find_first_non_white_space_character(line) 312 | else: 313 | col = min(col, ed.last_column(line)) 314 | 315 | return Position.new(line, col) 316 | 317 | static func move_to_first_non_white_space_character(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 318 | var i := ed.find_first_non_white_space_character(ed.curr_line()) 319 | return Position.new(cur.line, i) 320 | 321 | static func move_to_start_of_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 322 | return Position.new(cur.line, 0) 323 | 324 | static func move_to_end_of_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 325 | var line = cur.line 326 | if args.repeat > 1: 327 | line = ed.next_unfolded_line(line, args.repeat - 1) 328 | vim.current.last_h_pos = INF_COL 329 | return Position.new(line, ed.last_column(line)) 330 | 331 | static func move_to_top_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 332 | return Position.new(ed.first_visible_line(), cur.column) 333 | 334 | static func move_to_bottom_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 335 | return Position.new(ed.last_visible_line(), cur.column) 336 | 337 | static func move_to_middle_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 338 | var first := ed.first_visible_line() 339 | var count = ed.get_visible_line_count(first, ed.last_visible_line()) 340 | return Position.new(ed.next_unfolded_line(first, count / 2), cur.column) 341 | 342 | static func move_to_line_or_edge_of_document(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 343 | var line = ed.last_line() if args.forward else ed.first_line() 344 | if args.repeat_is_explicit: 345 | line = args.repeat + ed.first_line() - 1 346 | return Position.new(line, ed.find_first_non_white_space_character(line)) 347 | 348 | static func move_by_words(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 349 | var start_line := cur.line 350 | var start_col := cur.column 351 | var start_pos := cur.duplicate() 352 | 353 | # If we are beyond line end, move it to line end 354 | if start_col > 0 and start_col == ed.last_column(start_line) + 1: 355 | cur = Position.new(start_line, start_col-1) 356 | 357 | var forward : bool = args.forward 358 | var word_end : bool = args.word_end 359 | var big_word : bool = args.big_word 360 | var repeat : int = args.repeat 361 | var empty_line_is_word := not (forward and word_end) # For 'e', empty lines are not considered words 362 | 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 363 | 364 | if (forward and !word_end) or (not forward and word_end): # w or ge 365 | repeat += 1 366 | 367 | var words : Array[TextRange] = [] 368 | for i in range(repeat): 369 | var word = _find_word(cur, ed, forward, big_word, empty_line_is_word, one_line) 370 | if word != null: 371 | words.append(word) 372 | cur = Position.new(word.from.line, word.to.column-1 if forward else word.from.column) 373 | else: # eof 374 | words.append(TextRange.new(ed.last_pos(), ed.last_pos()) if forward else TextRange.zero) 375 | break 376 | 377 | var short_circuit : bool = len(words) != repeat 378 | var first_word := words[0] 379 | var last_word : TextRange = words.pop_back() 380 | if forward and not word_end: # w 381 | if vim.current.input_state.operator == "change": # cw need special treatment to not cover whitespaces 382 | if not short_circuit: 383 | last_word = words.pop_back() 384 | return last_word.to 385 | if not short_circuit and not start_pos.equals(first_word.from): 386 | last_word = words.pop_back() # We did not start in the middle of a word. Discard the extra word at the end. 387 | return last_word.from 388 | elif forward and word_end: # e 389 | return last_word.to.left() 390 | elif not forward and word_end: # ge 391 | if not short_circuit and not start_pos.equals(first_word.to.left()): 392 | last_word = words.pop_back() # We did not start in the middle of a word. Discard the extra word at the end. 393 | return last_word.to.left() 394 | else: # b 395 | return last_word.from 396 | 397 | static func move_to_matched_symbol(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 398 | # Get the symbol to match 399 | var symbol := ed.find_forward(cur.line, cur.column, func(c): return c.char in "()[]{}", true) 400 | if symbol == null: # No symbol found in this line after or under caret 401 | return null 402 | 403 | var counter_part : String = SYMBOLS[symbol.char] 404 | 405 | # Two attemps to find the symbol pair: from line start or doc start 406 | for from in [Position.new(symbol.line, 0), Position.new(0, 0)]: 407 | var parser = GDScriptParser.new(ed, from) 408 | if not parser.parse_until(symbol): 409 | continue 410 | 411 | if symbol.char in ")]}": 412 | parser.stack.reverse() 413 | for p in parser.stack: 414 | if p.char == counter_part: 415 | return p 416 | continue 417 | else: 418 | parser.parse_one_char() 419 | return parser.find_matching() 420 | return null 421 | 422 | static func move_to_next_char(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 423 | vim.last_char_search = args 424 | 425 | var forward : bool = args.forward 426 | var stop_before : bool = args.get("stop_before", false) 427 | var to_find = args.selected_character 428 | var repeat : int = args.repeat 429 | 430 | var old_pos := cur.duplicate() 431 | for ch in ed.chars(cur.line, cur.column + (1 if forward else -1), forward, true): 432 | if ch.char == to_find: 433 | repeat -= 1 434 | if repeat == 0: 435 | return old_pos if stop_before else Position.new(ch.line, ch.column) 436 | old_pos = Position.new(ch.line, ch.column) 437 | return null 438 | 439 | static func move_by_paragraph(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 440 | var search_dir: int = int(args.forward) - int(!args.forward) # 1 if forward else -1 441 | var line: int = cur.line + search_dir 442 | 443 | for i in range(args.repeat): 444 | while line >= 0 and line < ed.code_editor.get_line_count(): 445 | var current_empty: bool = ed.code_editor.get_line(line).strip_edges().is_empty() 446 | var prev_empty: bool = ed.code_editor.get_line(line - search_dir).strip_edges().is_empty() 447 | 448 | line += search_dir 449 | 450 | if current_empty and !prev_empty: 451 | break 452 | 453 | return Position.new(line - search_dir, 0) 454 | 455 | static func repeat_last_char_search(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 456 | var last_char_search := vim.last_char_search 457 | if last_char_search.is_empty(): 458 | return null 459 | args.forward = last_char_search.forward 460 | args.selected_character = last_char_search.selected_character 461 | args.stop_before = last_char_search.get("stop_before", false) 462 | args.inclusive = last_char_search.get("inclusive", false) 463 | return move_to_next_char(cur, args, ed, vim) 464 | 465 | static func expand_to_line(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 466 | return Position.new(cur.line + args.repeat - 1, INF_COL) 467 | 468 | static func find_word_under_caret(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 469 | var forward : bool = args.forward 470 | var range := ed.get_word_at_pos(cur.line, cur.column) 471 | var text := ed.range_text(range) 472 | var pos := ed.search(text, cur.line, cur.column + (1 if forward else -1), false, true, forward) 473 | vim.last_search_command = "*" if forward else "#" 474 | vim.search_buffer = text 475 | return pos 476 | 477 | static func find_again(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 478 | var forward : bool = args.forward 479 | forward = forward == (vim.last_search_command != "#") 480 | var case_sensitive := vim.last_search_command in "*#" 481 | var whole_word := vim.last_search_command in "*#" 482 | cur = cur.next(ed) if forward else cur.prev(ed) 483 | return ed.search(vim.search_buffer, cur.line, cur.column, case_sensitive, whole_word, forward) 484 | 485 | static func text_object(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Variant: 486 | var inner : bool = args.inner 487 | var obj : String = args.object 488 | 489 | if obj == "w" and inner: 490 | return ed.get_word_at_pos(cur.line, cur.column) 491 | 492 | if obj in "([{\"'": 493 | var counter_part : String = SYMBOLS[obj] 494 | for from in [Position.new(cur.line, 0), Position.new(0, 0)]: # Two attemps: from line beginning doc beginning 495 | var parser = GDScriptParser.new(ed, from) 496 | if not parser.parse_until(cur): 497 | continue 498 | 499 | var range = TextRange.zero 500 | if parser.stack_top_char == obj: 501 | range.from = parser.stack.back() 502 | range.to = parser.find_matching() 503 | elif ed.char_at(cur.line, cur.column) == obj: 504 | parser.parse_one_char() 505 | range.from = parser.pos 506 | range.to = parser.find_matching() 507 | else: 508 | continue 509 | 510 | if range.from == null or range.to == null: 511 | continue 512 | 513 | if inner: 514 | range.from = range.from.next(ed) 515 | else: 516 | range.to = range.to.next(ed) 517 | return range 518 | 519 | return null 520 | 521 | static func go_to_bookmark(cur: Position, args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Position: 522 | var name = args.selected_character 523 | var line := vim.current.bookmark_manager.get_bookmark(name) 524 | if line < 0: 525 | return null 526 | return Position.new(line, 0) 527 | 528 | ### OPERATORS 529 | 530 | static func delete(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 531 | var text := ed.selected_text() 532 | var line_wise = args.get("line_wise", false) 533 | vim.register.set_text(text, line_wise) 534 | 535 | ed.begin_complex_operation() 536 | ed.delete_selection() 537 | 538 | # For linewise delete, we want to delete one more line 539 | if line_wise: 540 | ed.select(ed.curr_line(), -1, ed.curr_line()+1, -1) 541 | ed.delete_selection() 542 | ed.end_complex_operation() 543 | 544 | var line := ed.curr_line() 545 | var col := ed.curr_column() 546 | if col > ed.last_column(line): # If after deletion we are beyond the end, move left 547 | ed.set_curr_column(ed.last_column(line)) 548 | 549 | static func yank(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 550 | var text := ed.selected_text() 551 | ed.deselect() 552 | vim.register.set_text(text, args.get("line_wise", false)) 553 | 554 | static func change(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 555 | var text := ed.selected_text() 556 | vim.register.set_text(text, args.get("line_wise", false)) 557 | 558 | vim.current.enter_insert_mode(); 559 | ed.delete_selection() 560 | 561 | static func change_case(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 562 | var lower_case : bool = args.get("lower", false) 563 | var text := ed.selected_text() 564 | ed.replace_selection(text.to_lower() if lower_case else text.to_upper()) 565 | 566 | static func toggle_case(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 567 | var text := ed.selected_text() 568 | var s := PackedStringArray() 569 | for c in text: 570 | s.append(c.to_lower() if c == c.to_upper() else c.to_upper()) 571 | ed.replace_selection(''.join(s)) 572 | 573 | 574 | ### ACTIONS 575 | 576 | static func paste(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 577 | var after : bool = args.after 578 | var line_wise := vim.register.line_wise 579 | var clipboard_text := vim.register.text 580 | 581 | var text : String = "" 582 | for i in range(args.repeat): 583 | text += clipboard_text 584 | 585 | var line := ed.curr_line() 586 | var col := ed.curr_column() 587 | 588 | ed.begin_complex_operation() 589 | if vim.current.visual_mode: 590 | ed.delete_selection() 591 | else: 592 | if line_wise: 593 | if after: 594 | text = "\n" + text 595 | col = len(ed.line_text(line)) 596 | else: 597 | text = text + "\n" 598 | col = 0 599 | else: 600 | col += 1 if after else 0 601 | 602 | ed.set_curr_column(col) 603 | 604 | ed.insert_text(text) 605 | ed.end_complex_operation() 606 | 607 | static func undo(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 608 | for i in range(args.repeat): 609 | ed.undo() 610 | ed.deselect() 611 | 612 | static func redo(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 613 | for i in range(args.repeat): 614 | ed.redo() 615 | ed.deselect() 616 | 617 | static func replace(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 618 | var to_replace = args.selected_character 619 | var line := ed.curr_line() 620 | var col := ed.curr_column() 621 | ed.select(line, col, line, col) 622 | ed.replace_selection(to_replace) 623 | 624 | static func enter_insert_mode(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 625 | var insert_at : String = args.insert_at 626 | var line = ed.curr_line() 627 | var col = ed.curr_column() 628 | 629 | vim.current.enter_insert_mode() 630 | 631 | match insert_at: 632 | "inplace": 633 | pass 634 | "after": 635 | ed.set_curr_column(col + 1) 636 | "bol": 637 | ed.set_curr_column(ed.find_first_non_white_space_character(line)) 638 | "eol": 639 | ed.set_curr_column(INF_COL) 640 | "new_line_below": 641 | ed.set_curr_column(INF_COL) 642 | ed.simulate_press(KEY_ENTER) 643 | "new_line_above": 644 | ed.set_curr_column(0) 645 | if line == ed.first_line(): 646 | ed.insert_text("\n") 647 | ed.jump_to(0, 0) 648 | else: 649 | ed.jump_to(line - 1, INF_COL) 650 | ed.simulate_press(KEY_ENTER) 651 | 652 | static func enter_visual_mode(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 653 | var line_wise : bool = args.get('line_wise', false) 654 | vim.current.enter_visual_mode(line_wise) 655 | 656 | static func search(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 657 | if OS.get_name() == "macOS": 658 | ed.simulate_press(KEY_F, 0, false, false, false, true) 659 | else: 660 | ed.simulate_press(KEY_F, 0, true, false, false, false) 661 | vim.last_search_command = "/" 662 | 663 | static func jump_list_walk(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 664 | var offset : int = args.repeat * (1 if args.forward else -1) 665 | var pos : Position = vim.current.jump_list.move(offset) 666 | if pos != null: 667 | if not args.forward: 668 | vim.current.jump_list.set_next(ed.curr_position()) 669 | ed.jump_to(pos.line, pos.column) 670 | 671 | static func toggle_folding(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 672 | ed.toggle_folding(ed.curr_line()) 673 | 674 | static func fold_all(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 675 | ed.fold_all() 676 | 677 | static func unfold_all(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 678 | ed.unfold_all() 679 | 680 | static func repeat_last_edit(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 681 | var repeat : int = args.repeat 682 | vim.macro_manager.play_macro(repeat, ".", ed) 683 | 684 | static func record_macro(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 685 | var name = args.selected_character 686 | if name in ALPHANUMERIC: 687 | vim.macro_manager.start_record_macro(name) 688 | 689 | static func stop_record_macro(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 690 | vim.macro_manager.stop_record_macro() 691 | 692 | static func play_macro(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 693 | var name = args.selected_character 694 | var repeat : int = args.repeat 695 | if name in ALPHANUMERIC: 696 | vim.macro_manager.play_macro(repeat, name, ed) 697 | 698 | static func is_recording(ed: EditorAdaptor, vim: Vim) -> bool: 699 | return vim.macro_manager.is_recording() 700 | 701 | static func indent(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 702 | var repeat : int = args.repeat 703 | var forward : bool = args.get("forward", false) 704 | var range = ed.selection() 705 | 706 | if not range.is_single_line() and range.to.column == 0: # Don't select the last empty line 707 | ed.select(range.from.line, range.from.column, range.to.line-1, INF_COL) 708 | 709 | ed.begin_complex_operation() 710 | for i in range(repeat): 711 | if forward: 712 | ed.indent() 713 | else: 714 | ed.unindent() 715 | ed.end_complex_operation() 716 | 717 | static func join_lines(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 718 | if vim.current.normal_mode: 719 | var line := ed.curr_line() 720 | ed.select(line, 0, line + args.repeat, INF_COL) 721 | 722 | var range := ed.selection() 723 | ed.select(range.from.line, 0, range.to.line, INF_COL) 724 | var s := PackedStringArray() 725 | s.append(ed.line_text(range.from.line)) 726 | for line in range(range.from.line + 1, range.to.line + 1): 727 | s.append(ed.line_text(line).lstrip(' \t\n')) 728 | ed.replace_selection(' '.join(s)) 729 | 730 | static func set_bookmark(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 731 | var name = args.selected_character 732 | if name in LOWER_ALPHA: 733 | vim.current.bookmark_manager.set_bookmark(name, ed.curr_line()) 734 | 735 | static func go_to_definition(args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 736 | var pos_before := ed.curr_position() 737 | 738 | ed.go_to_definition() 739 | 740 | await ed.code_editor.get_tree().process_frame 741 | var pos_after := ed.curr_position() 742 | if not pos_before.equals(pos_after): 743 | vim.current.jump_list.add(pos_before, pos_after) 744 | 745 | ### HELPER FUNCTIONS 746 | 747 | ## 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. 748 | ## 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. 749 | static func _find_word(cur: Position, ed: EditorAdaptor, forward: bool, big_word: bool, empty_line_is_word: bool, one_line: bool) -> TextRange: 750 | 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 ] 751 | 752 | for p in ed.chars(cur.line, cur.column, forward): 753 | if one_line and p.char == '\n': # If we only allow search in one line and we met the line end 754 | return TextRange.from_num3(p.line, p.column, INF_COL) 755 | 756 | if p.line != cur.line and empty_line_is_word and p.line_text.strip_edges() == '': 757 | return TextRange.from_num3(p.line, 0, 0) 758 | 759 | for char_test in char_tests: 760 | if char_test.call(p.char): 761 | var word_start := p.column 762 | var word_end := word_start 763 | for q in ed.chars(p.line, p.column, forward, true): # Advance to end of word. 764 | if not char_test.call(q.char): 765 | break 766 | word_end = q.column 767 | 768 | if p.line == cur.line and word_start == cur.column and word_end == word_start: 769 | continue # We started at the end of a word. Find the next one. 770 | else: 771 | return TextRange.from_num3(p.line, min(word_start, word_end), max(word_start + 1, word_end + 1)) 772 | return null 773 | 774 | 775 | class Position: 776 | var line: int 777 | var column: int 778 | 779 | static var zero :Position: 780 | get: 781 | return Position.new(0, 0) 782 | 783 | func _init(l: int, c: int): 784 | line = l 785 | column = c 786 | 787 | func _to_string() -> String: 788 | return "(%s, %s)" % [line, column] 789 | 790 | func equals(other: Position) -> bool: 791 | return line == other.line and column == other.column 792 | 793 | func compares_to(other: Position) -> int: 794 | if line < other.line: return -1 795 | if line > other.line: return 1 796 | if column < other.column: return -1 797 | if column > other.column: return 1 798 | return 0 799 | 800 | func duplicate() -> Position: return Position.new(line, column) 801 | func up() -> Position: return Position.new(line-1, column) 802 | func down() -> Position: return Position.new(line+1, column) 803 | func left() -> Position: return Position.new(line, column-1) 804 | func right() -> Position: return Position.new(line, column+1) 805 | func next(ed: EditorAdaptor) -> Position: return ed.offset_pos(self, 1) 806 | func prev(ed: EditorAdaptor) -> Position: return ed.offset_pos(self, -1) 807 | 808 | 809 | class TextRange: 810 | var from: Position 811 | var to: Position 812 | 813 | static var zero : TextRange: 814 | get: 815 | return TextRange.new(Position.zero, Position.zero) 816 | 817 | static func from_num4(from_line: int, from_column: int, to_line: int, to_column: int): 818 | return TextRange.new(Position.new(from_line, from_column), Position.new(to_line, to_column)) 819 | 820 | static func from_num3(line: int, from_column: int, to_column: int): 821 | return from_num4(line, from_column, line, to_column) 822 | 823 | func _init(f: Position, t: Position): 824 | from = f 825 | to = t 826 | 827 | func _to_string() -> String: 828 | return "[%s - %s]" % [from, to] 829 | 830 | func is_single_line() -> bool: 831 | return from.line == to.line 832 | 833 | func is_empty() -> bool: 834 | return from.line == to.line and from.column == to.column 835 | 836 | 837 | class CharPos extends Position: 838 | var line_text : String 839 | 840 | var char: String: 841 | get: 842 | return line_text[column] if column < len(line_text) else '\n' 843 | 844 | func _init(line_text: String, line: int, col: int): 845 | super(line, col) 846 | self.line_text = line_text 847 | 848 | 849 | class JumpList: 850 | var buffer: Array[Position] 851 | var pointer: int = 0 852 | 853 | func _init(capacity: int = 20): 854 | buffer = [] 855 | buffer.resize(capacity) 856 | 857 | func add(old_pos: Position, new_pos: Position) -> void: 858 | var current : Position = buffer[pointer] 859 | if current == null or not current.equals(old_pos): 860 | pointer = (pointer + 1) % len(buffer) 861 | buffer[pointer] = old_pos 862 | pointer = (pointer + 1) % len(buffer) 863 | buffer[pointer] = new_pos 864 | 865 | func set_next(pos: Position) -> void: 866 | buffer[(pointer + 1) % len(buffer)] = pos # This overrides next forward position (TODO: an insert might be better) 867 | 868 | func move(offset: int) -> Position: 869 | var t := (pointer + offset) % len(buffer) 870 | var r : Position = buffer[t] 871 | if r != null: 872 | pointer = t 873 | return r 874 | 875 | func on_lines_edited(from: int, to: int) -> void: 876 | for pos in buffer: 877 | if pos != null and pos.line > from: # Unfortunately we don't know which column changed 878 | pos.line += to - from 879 | 880 | 881 | class InputState: 882 | var prefix_repeat: String 883 | var motion_repeat: String 884 | var operator: String 885 | var operator_args: Dictionary 886 | var buffer: Array[InputEventKey] = [] 887 | 888 | func push_key(key: InputEventKey) -> void: 889 | buffer.append(key) 890 | 891 | func push_repeat_digit(d: String) -> void: 892 | if operator.is_empty(): 893 | prefix_repeat += d 894 | else: 895 | motion_repeat += d 896 | 897 | func get_repeat() -> int: 898 | var repeat : int = 0 899 | if prefix_repeat: 900 | repeat = max(repeat, 1) * int(prefix_repeat) 901 | if motion_repeat: 902 | repeat = max(repeat, 1) * int(motion_repeat) 903 | return repeat 904 | 905 | func key_codes() -> Array[String]: 906 | var r : Array[String] = [] 907 | for k in buffer: 908 | r.append(k.as_text_keycode()) 909 | return r 910 | 911 | func clear() -> void: 912 | prefix_repeat = "" 913 | motion_repeat = "" 914 | operator = "" 915 | buffer.clear() 916 | 917 | 918 | class GDScriptParser: 919 | const open_symbol := { "(": ")", "[": "]", "{": "}", "'": "'", '"': '"' } 920 | const close_symbol := { ")": "(", "]": "[", "}": "{", } 921 | 922 | var stack : Array[CharPos] 923 | var in_comment := false 924 | var escape_count := 0 925 | var valid: bool = true 926 | var eof : bool = false 927 | var pos: Position 928 | 929 | var stack_top_char : String: 930 | get: 931 | return "" if stack.is_empty() else stack.back().char 932 | 933 | var _it: CharIterator 934 | var _ed : EditorAdaptor 935 | 936 | func _init(ed: EditorAdaptor, from: Position): 937 | _ed = ed 938 | _it = ed.chars(from.line, from.column) 939 | if not _it._iter_init(null): 940 | eof = true 941 | 942 | func parse_until(to: Position) -> bool: 943 | while valid and not eof: 944 | parse_one_char() 945 | if _it.line == to.line and _it.column == to.column: 946 | break 947 | return valid and not eof 948 | 949 | 950 | func find_matching() -> Position: 951 | var depth := len(stack) 952 | while valid and not eof: 953 | parse_one_char() 954 | if len(stack) < depth: 955 | return pos 956 | return null 957 | 958 | func parse_one_char() -> String: # ChatGPT got credit here 959 | if eof or not valid: 960 | return "" 961 | 962 | var p := _it._iter_get(null) 963 | pos = p 964 | 965 | if not _it._iter_next(null): 966 | eof = true 967 | 968 | var char := p.char 969 | var top: String = '' if stack.is_empty() else stack.back().char 970 | if top in "'\"": # in string 971 | if char == top and escape_count % 2 == 0: 972 | stack.pop_back() 973 | escape_count = 0 974 | return char 975 | escape_count = escape_count + 1 if char == "\\" else 0 976 | elif in_comment: 977 | if char == "\n": 978 | in_comment = false 979 | elif char == "#": 980 | in_comment = true 981 | elif char in open_symbol: 982 | stack.append(p) 983 | return char 984 | elif char in close_symbol: 985 | if top == close_symbol[char]: 986 | stack.pop_back() 987 | return char 988 | else: 989 | valid = false 990 | return "" 991 | 992 | 993 | class Register: 994 | var line_wise : bool = false 995 | var text : String: 996 | get: 997 | return DisplayServer.clipboard_get() 998 | 999 | func set_text(value: String, line_wise: bool) -> void: 1000 | self.line_wise = line_wise 1001 | DisplayServer.clipboard_set(value) 1002 | 1003 | 1004 | class BookmarkManager: 1005 | var bookmarks : Dictionary 1006 | 1007 | func on_lines_edited(from: int, to: int) -> void: 1008 | for b in bookmarks: 1009 | var line : int = bookmarks[b] 1010 | if line > from: 1011 | bookmarks[b] += to - from 1012 | 1013 | func set_bookmark(name: String, line: int) -> void: 1014 | bookmarks[name] = line 1015 | 1016 | func get_bookmark(name: String) -> int: 1017 | return bookmarks.get(name, -1) 1018 | 1019 | 1020 | class CommandMatchResult: 1021 | var full: Array[Dictionary] = [] 1022 | var partial: Array[Dictionary] = [] 1023 | 1024 | 1025 | class VimSession: 1026 | var ed : EditorAdaptor 1027 | 1028 | ## Mode insert_mode | visual_mode | visual_line 1029 | ## Insert true | false | false 1030 | ## Normal false | false | false 1031 | ## Visual false | true | false 1032 | ## Visual Line false | true | true 1033 | var insert_mode : bool = false 1034 | var visual_mode : bool = false 1035 | var visual_line : bool = false 1036 | 1037 | var normal_mode: bool: 1038 | get: 1039 | return not insert_mode and not visual_mode 1040 | 1041 | ## Pending input 1042 | var input_state := InputState.new() 1043 | 1044 | ## The last motion occurred 1045 | var last_motion : String 1046 | 1047 | ## 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. 1048 | ## If j is pressed again and cursor goes to the next line, the cursor should go back to its horizontal position on the longer 1049 | ## line if it can. This is to keep track of the horizontal position. 1050 | var last_h_pos : int = 0 1051 | 1052 | ## How many times text are changed 1053 | var text_change_number : int 1054 | 1055 | ## List of positions for C-I and C-O 1056 | var jump_list := JumpList.new() 1057 | 1058 | ## The bookmark manager of the session 1059 | var bookmark_manager := BookmarkManager.new() 1060 | 1061 | ## The start position of visual mode 1062 | var visual_start_pos := Position.zero 1063 | 1064 | func enter_normal_mode() -> void: 1065 | if insert_mode: 1066 | ed.end_complex_operation() # Wrap up the undo operation when we get out of insert mode 1067 | 1068 | insert_mode = false 1069 | visual_mode = false 1070 | visual_line = false 1071 | ed.set_block_caret(true) 1072 | 1073 | func enter_insert_mode() -> void: 1074 | insert_mode = true 1075 | visual_mode = false 1076 | visual_line = false 1077 | ed.set_block_caret(false) 1078 | ed.begin_complex_operation() 1079 | 1080 | func enter_visual_mode(line_wise: bool) -> void: 1081 | insert_mode = false 1082 | visual_mode = true 1083 | visual_line = line_wise 1084 | ed.set_block_caret(true) 1085 | 1086 | visual_start_pos = ed.curr_position() 1087 | 1088 | if line_wise: 1089 | ed.select(visual_start_pos.line, 0, visual_start_pos.line, INF_COL) 1090 | else: 1091 | ed.select_by_pos2(visual_start_pos, visual_start_pos) 1092 | 1093 | 1094 | class Macro: 1095 | var keys : Array[InputEventKey] = [] 1096 | var enabled := false 1097 | 1098 | func _to_string() -> String: 1099 | var s := PackedStringArray() 1100 | for key in keys: 1101 | s.append(key.as_text_keycode()) 1102 | return ",".join(s) 1103 | 1104 | func play(ed: EditorAdaptor) -> void: 1105 | for key in keys: 1106 | ed.simulate_press_key(key) 1107 | ed.simulate_press(KEY_ESCAPE) 1108 | 1109 | 1110 | class MacroManager: 1111 | var vim : Vim 1112 | var macros : Dictionary = {} 1113 | var recording_name : String 1114 | var playing_names : Array[String] = [] 1115 | var command_buffer: Array[InputEventKey] 1116 | 1117 | func _init(vim: Vim): 1118 | self.vim = vim 1119 | 1120 | func start_record_macro(name: String): 1121 | print('Recording macro "%s"...' % name ) 1122 | macros[name] = Macro.new() 1123 | recording_name = name 1124 | 1125 | func stop_record_macro() -> void: 1126 | print('Stop recording macro "%s"' % recording_name) 1127 | macros[recording_name].enabled = true 1128 | recording_name = "" 1129 | 1130 | func is_recording() -> bool: 1131 | return recording_name != "" 1132 | 1133 | func play_macro(n: int, name: String, ed: EditorAdaptor) -> void: 1134 | var macro : Macro = macros.get(name, null) 1135 | if (macro == null or not macro.enabled): 1136 | return 1137 | if name in playing_names: 1138 | return # to avoid recursion 1139 | 1140 | playing_names.append(name) 1141 | if len(playing_names) == 1: 1142 | ed.begin_complex_operation() 1143 | 1144 | if DEBUGGING: 1145 | print("Playing macro %s: %s" % [name, macro]) 1146 | 1147 | for i in range(n): 1148 | macro.play(ed) 1149 | 1150 | ed.simulate_press(KEY_NONE, CODE_MACRO_PLAY_END) # This special marks the end of macro play 1151 | 1152 | func on_macro_finished(ed: EditorAdaptor): 1153 | var name : String = playing_names.pop_back() 1154 | if playing_names.is_empty(): 1155 | ed.end_complex_operation() 1156 | 1157 | func push_key(key: InputEventKey) -> void: 1158 | command_buffer.append(key) 1159 | if recording_name: 1160 | macros[recording_name].keys.append(key) 1161 | 1162 | func on_command_processed(command: Dictionary, is_edit: bool) -> void: 1163 | if is_edit and command.get('action', '') != "repeat_last_edit": 1164 | var macro := Macro.new() 1165 | macro.keys = command_buffer.duplicate() 1166 | macro.enabled = true 1167 | macros["."] = macro 1168 | command_buffer.clear() 1169 | 1170 | 1171 | ## Global VIM state; has multiple sessions 1172 | class Vim: 1173 | var sessions : Dictionary 1174 | var current: VimSession 1175 | var register: Register = Register.new() 1176 | var last_char_search: Dictionary = {} # { selected_character, stop_before, forward, inclusive } 1177 | var last_search_command: String 1178 | var search_buffer: String 1179 | var macro_manager := MacroManager.new(self) 1180 | 1181 | func set_current_session(s: Script, ed: EditorAdaptor): 1182 | var session : VimSession = sessions.get(s) 1183 | if not session: 1184 | session = VimSession.new() 1185 | session.ed = ed 1186 | sessions[s] = session 1187 | current = session 1188 | 1189 | func remove_session(s: Script): 1190 | sessions.erase(s) 1191 | 1192 | 1193 | class CharIterator: 1194 | var ed : EditorAdaptor 1195 | var line : int 1196 | var column : int 1197 | var forward : bool 1198 | var one_line : bool 1199 | var line_text : String 1200 | 1201 | func _init(ed: EditorAdaptor, line: int, col: int, forward: bool, one_line: bool): 1202 | self.ed = ed 1203 | self.line = line 1204 | self.column = col 1205 | self.forward = forward 1206 | self.one_line = one_line 1207 | 1208 | func _ensure_column_valid() -> bool: 1209 | if column < 0 or column > len(line_text): 1210 | line += 1 if forward else -1 1211 | if one_line or line < 0 or line > ed.last_line(): 1212 | return false 1213 | line_text = ed.line_text(line) 1214 | column = 0 if forward else len(line_text) 1215 | return true 1216 | 1217 | func _iter_init(arg) -> bool: 1218 | if line < 0 or line > ed.last_line(): 1219 | return false 1220 | line_text = ed.line_text(line) 1221 | return _ensure_column_valid() 1222 | 1223 | func _iter_next(arg) -> bool: 1224 | column += 1 if forward else -1 1225 | return _ensure_column_valid() 1226 | 1227 | func _iter_get(arg) -> CharPos: 1228 | return CharPos.new(line_text, line, column) 1229 | 1230 | 1231 | class EditorAdaptor: 1232 | var code_editor: CodeEdit 1233 | var tab_width : int = 4 1234 | var complex_ops : int = 0 1235 | 1236 | func set_code_editor(new_editor: CodeEdit) -> void: 1237 | self.code_editor = new_editor 1238 | 1239 | func notify_settings_changed(settings: EditorSettings) -> void: 1240 | tab_width = settings.get_setting("text_editor/behavior/indent/size") as int 1241 | 1242 | func curr_position() -> Position: 1243 | return Position.new(code_editor.get_caret_line(), code_editor.get_caret_column()) 1244 | 1245 | func curr_line() -> int: 1246 | return code_editor.get_caret_line() 1247 | 1248 | func curr_column() -> int: 1249 | return code_editor.get_caret_column() 1250 | 1251 | func set_curr_column(col: int) -> void: 1252 | code_editor.set_caret_column(col) 1253 | 1254 | func jump_to(line: int, col: int) -> void: 1255 | code_editor.unfold_line(line) 1256 | 1257 | if line < first_visible_line(): 1258 | code_editor.set_line_as_first_visible(max(0, line-8)) 1259 | elif line > last_visible_line(): 1260 | code_editor.set_line_as_last_visible(min(last_line(), line+8)) 1261 | code_editor.set_caret_line(line) 1262 | code_editor.set_caret_column(col) 1263 | 1264 | func first_line() -> int: 1265 | return 0 1266 | 1267 | func last_line() -> int : 1268 | return code_editor.get_line_count() - 1 1269 | 1270 | func first_visible_line() -> int: 1271 | return code_editor.get_first_visible_line() 1272 | 1273 | func last_visible_line() -> int: 1274 | return code_editor.get_last_full_visible_line() 1275 | 1276 | func get_visible_line_count(from_line: int, to_line: int) -> int: 1277 | return code_editor.get_visible_line_count_in_range(from_line, to_line) 1278 | 1279 | func next_unfolded_line(line: int, offset: int = 1, forward: bool = true) -> int: 1280 | var step : int = 1 if forward else -1 1281 | if line + step > last_line() or line + step < first_line(): 1282 | return line 1283 | 1284 | var count := code_editor.get_next_visible_line_offset_from(line + step, offset * step) 1285 | return line + count * (1 if forward else -1) 1286 | 1287 | func last_column(line: int = -1) -> int: 1288 | if line == -1: 1289 | line = curr_line() 1290 | return len(line_text(line)) - 1 1291 | 1292 | func last_pos() -> Position: 1293 | var line = last_line() 1294 | return Position.new(line, last_column(line)) 1295 | 1296 | func line_text(line: int) -> String: 1297 | return code_editor.get_line(line) 1298 | 1299 | func range_text(range: TextRange) -> String: 1300 | var s := PackedStringArray() 1301 | for p in chars(range.from.line, range.from.column): 1302 | if p.equals(range.to): 1303 | break 1304 | s.append(p.char) 1305 | return "".join(s) 1306 | 1307 | func char_at(line: int, col: int) -> String: 1308 | var s := line_text(line) 1309 | return s[col] if col >= 0 and col < len(s) else '' 1310 | 1311 | func go_to_definition() -> void: 1312 | var symbol := code_editor.get_word_under_caret() 1313 | code_editor.symbol_lookup.emit(symbol, curr_line(), curr_column()) 1314 | 1315 | func set_block_caret(block: bool) -> void: 1316 | if block: 1317 | if curr_column() == last_column() + 1: 1318 | code_editor.caret_type = TextEdit.CARET_TYPE_LINE 1319 | code_editor.add_theme_constant_override("caret_width", 8) 1320 | else: 1321 | code_editor.caret_type = TextEdit.CARET_TYPE_BLOCK 1322 | code_editor.add_theme_constant_override("caret_width", 1) 1323 | else: 1324 | code_editor.add_theme_constant_override("caret_width", 1) 1325 | code_editor.caret_type = TextEdit.CARET_TYPE_LINE 1326 | 1327 | func deselect() -> void: 1328 | code_editor.deselect() 1329 | 1330 | func select_by_pos2(from: Position, to: Position) -> void: 1331 | select(from.line, from.column, to.line, to.column) 1332 | 1333 | func select(from_line: int, from_col: int, to_line: int, to_col: int) -> void: 1334 | # If we try to select backward pass the first line, select the first char 1335 | if to_line < 0: 1336 | to_line = 0 1337 | to_col = 0 1338 | # If we try to select pass the last line, select till the last char 1339 | elif to_line > last_line(): 1340 | to_line = last_line() 1341 | to_col = INF_COL 1342 | 1343 | # Our select() is inclusive, e.g. ed.select(0, 0, 0, 0) selects the first character; 1344 | # while CodeEdit's select() is exlusvie on the right hand side, e.g. code_editor.select(0, 0, 0, 1) selects the first character. 1345 | # We do the translation here: 1346 | if from_line < to_line or (from_line == to_line and from_col <= to_col): # Selecting forward 1347 | to_col += 1 1348 | else: 1349 | from_col += 1 1350 | 1351 | if DEBUGGING: 1352 | print(" Selecting from (%d,%d) to (%d,%d)" % [from_line, from_col, to_line, to_col]) 1353 | 1354 | code_editor.select(from_line, from_col, to_line, to_col) 1355 | 1356 | func delete_selection() -> void: 1357 | code_editor.delete_selection() 1358 | 1359 | func selected_text() -> String: 1360 | return code_editor.get_selected_text() 1361 | 1362 | func selection() -> TextRange: 1363 | var from := Position.new(code_editor.get_selection_from_line(), code_editor.get_selection_from_column()) 1364 | var to := Position.new(code_editor.get_selection_to_line(), code_editor.get_selection_to_column()) 1365 | return TextRange.new(from, to) 1366 | 1367 | func replace_selection(text: String) -> void: 1368 | var col := curr_column() 1369 | begin_complex_operation() 1370 | delete_selection() 1371 | insert_text(text) 1372 | end_complex_operation() 1373 | set_curr_column(col) 1374 | 1375 | func toggle_folding(line_or_above: int) -> void: 1376 | if code_editor.is_line_folded(line_or_above): 1377 | code_editor.unfold_line(line_or_above) 1378 | else: 1379 | while line_or_above >= 0: 1380 | if code_editor.can_fold_line(line_or_above): 1381 | code_editor.fold_line(line_or_above) 1382 | break 1383 | line_or_above -= 1 1384 | 1385 | func fold_all() -> void: 1386 | code_editor.fold_all_lines() 1387 | 1388 | func unfold_all() -> void: 1389 | code_editor.unfold_all_lines() 1390 | 1391 | func insert_text(text: String) -> void: 1392 | code_editor.insert_text_at_caret(text) 1393 | 1394 | func offset_pos(pos: Position, offset: int) -> Position: 1395 | var count : int = abs(offset) + 1 1396 | for p in chars(pos.line, pos.column, offset > 0): 1397 | count -= 1 1398 | if count == 0: 1399 | return p 1400 | return null 1401 | 1402 | func undo() -> void: 1403 | code_editor.undo() 1404 | 1405 | func redo() -> void: 1406 | code_editor.redo() 1407 | 1408 | func indent() -> void: 1409 | code_editor.indent_lines() 1410 | 1411 | func unindent() -> void: 1412 | code_editor.unindent_lines() 1413 | 1414 | func simulate_press_key(key: InputEventKey): 1415 | for pressed in [true, false]: 1416 | var key2 := key.duplicate() 1417 | key2.pressed = pressed 1418 | Input.parse_input_event(key2) 1419 | 1420 | func simulate_press(keycode: Key, unicode: int=0, ctrl=false, alt=false, shift=false, meta=false) -> void: 1421 | var k = InputEventKey.new() 1422 | if ctrl: 1423 | k.ctrl_pressed = true 1424 | if shift: 1425 | k.shift_pressed = true 1426 | if alt: 1427 | k.alt_pressed = true 1428 | if meta: 1429 | k.meta_pressed = true 1430 | k.keycode = keycode 1431 | k.key_label = keycode 1432 | k.unicode = unicode 1433 | simulate_press_key(k) 1434 | 1435 | func begin_complex_operation() -> void: 1436 | complex_ops += 1 1437 | if complex_ops == 1: 1438 | if DEBUGGING: 1439 | print("Complex operation begins") 1440 | code_editor.begin_complex_operation() 1441 | 1442 | func end_complex_operation() -> void: 1443 | complex_ops -= 1 1444 | if complex_ops == 0: 1445 | if DEBUGGING: 1446 | print("Complex operation ends") 1447 | code_editor.end_complex_operation() 1448 | 1449 | ## Return the index of the first non whtie space character in string 1450 | func find_first_non_white_space_character(line: int) -> int: 1451 | var s := line_text(line) 1452 | return len(s) - len(s.lstrip(" \t\r\n")) 1453 | 1454 | ## Return the next (or previous) char from current position and update current position according. Return "" if not more char available 1455 | func chars(line: int, col: int, forward: bool = true, one_line: bool = false) -> CharIterator: 1456 | return CharIterator.new(self, line, col, forward, one_line) 1457 | 1458 | func find_forward(line: int, col: int, condition: Callable, one_line: bool = false) -> CharPos: 1459 | for p in chars(line, col, true, one_line): 1460 | if condition.call(p): 1461 | return p 1462 | return null 1463 | 1464 | func find_backforward(line: int, col: int, condition: Callable, one_line: bool = false) -> CharPos: 1465 | for p in chars(line, col, false, one_line): 1466 | if condition.call(p): 1467 | return p 1468 | return null 1469 | 1470 | func get_word_at_pos(line: int, col: int) -> TextRange: 1471 | var end := find_forward(line, col, func(p): return p.char not in ALPHANUMERIC, true); 1472 | var start := find_backforward(line, col, func(p): return p.char not in ALPHANUMERIC, true); 1473 | return TextRange.new(start.right(), end) 1474 | 1475 | func search(text: String, line: int, col: int, match_case: bool, whole_word: bool, forward: bool) -> Position: 1476 | var flags : int = 0 1477 | if match_case: flags |= TextEdit.SEARCH_MATCH_CASE 1478 | if whole_word: flags |= TextEdit.SEARCH_WHOLE_WORDS 1479 | if not forward: flags |= TextEdit.SEARCH_BACKWARDS 1480 | var result = code_editor.search(text, flags, line, col) 1481 | if result.x < 0 or result. y < 0: 1482 | return null 1483 | 1484 | code_editor.set_search_text(text) 1485 | return Position.new(result.y, result.x) 1486 | 1487 | func has_focus() -> bool: 1488 | return weakref(code_editor).get_ref() and code_editor.has_focus() 1489 | 1490 | 1491 | class CommandDispatcher: 1492 | var key_map : Array[Dictionary] 1493 | 1494 | func _init(km: Array[Dictionary]): 1495 | self.key_map = km 1496 | 1497 | func dispatch(key: InputEventKey, vim: Vim, ed: EditorAdaptor) -> bool: 1498 | var key_code := key.as_text_keycode() 1499 | var input_state := vim.current.input_state 1500 | 1501 | vim.macro_manager.push_key(key) 1502 | 1503 | if key_code == "Escape" or key_code == "Ctrl+BracketLeft": 1504 | input_state.clear() 1505 | vim.macro_manager.on_command_processed({}, vim.current.insert_mode) # From insert mode to normal mode, this marks the end of an edit command 1506 | vim.current.enter_normal_mode() 1507 | return false # Let godot get the Esc as well to dispose code completion pops, etc 1508 | 1509 | if vim.current.insert_mode: # We are in insert mode 1510 | return false # Let Godot CodeEdit handle it 1511 | 1512 | if key_code not in ["Shift", "Ctrl", "Alt", "Escape"]: # Don't add these to input buffer 1513 | # Handle digits 1514 | if key_code.is_valid_int() and input_state.buffer.is_empty(): 1515 | input_state.push_repeat_digit(key_code) 1516 | if input_state.get_repeat() > 0: # No more handding if it is only repeat digit 1517 | return true 1518 | 1519 | # Save key to buffer 1520 | input_state.push_key(key) 1521 | 1522 | # Match the command 1523 | var context = Context.VISUAL if vim.current.visual_mode else Context.NORMAL 1524 | var result = match_commands(context, vim.current.input_state, ed, vim) 1525 | if not result.full.is_empty(): 1526 | var command = result.full[0].duplicate(true) 1527 | var change_num := vim.current.text_change_number 1528 | if process_command(command, ed, vim): 1529 | input_state.clear() 1530 | if vim.current.normal_mode: 1531 | vim.macro_manager.on_command_processed(command, vim.current.text_change_number > change_num) # Notify macro manager about the finished command 1532 | elif result.partial.is_empty(): 1533 | input_state.clear() 1534 | 1535 | return true # We handled the input 1536 | 1537 | func match_commands(context: Context, input_state: InputState, ed: EditorAdaptor, vim: Vim) -> CommandMatchResult: 1538 | # Partial matches are not applied. They inform the key handler 1539 | # that the current key sequence is a subsequence of a valid key 1540 | # sequence, so that the key buffer is not cleared. 1541 | var result := CommandMatchResult.new() 1542 | var pressed := input_state.key_codes() 1543 | 1544 | for command in key_map: 1545 | if not is_command_available(command, context, ed, vim): 1546 | continue 1547 | 1548 | var mapped : Array = command.keys 1549 | if mapped[-1] == "{char}": 1550 | if pressed.slice(0, -1) == mapped.slice(0, -1) and len(pressed) == len(mapped): 1551 | result.full.append(command) 1552 | elif mapped.slice(0, len(pressed)-1) == pressed.slice(0, -1): 1553 | result.partial.append(command) 1554 | else: 1555 | continue 1556 | else: 1557 | if pressed == mapped: 1558 | result.full.append(command) 1559 | elif mapped.slice(0, len(pressed)) == pressed: 1560 | result.partial.append(command) 1561 | else: 1562 | continue 1563 | 1564 | return result 1565 | 1566 | func is_command_available(command: Dictionary, context: Context, ed: EditorAdaptor, vim: Vim) -> bool: 1567 | if command.get("context") not in [null, context]: 1568 | return false 1569 | 1570 | var when : String = command.get("when", '') 1571 | if when and not Callable(Command, when).call(ed, vim): 1572 | return false 1573 | 1574 | var when_not: String = command.get("when_not", '') 1575 | if when_not and Callable(Command, when_not).call(ed, vim): 1576 | return false 1577 | 1578 | return true 1579 | 1580 | func process_command(command: Dictionary, ed: EditorAdaptor, vim: Vim) -> bool: 1581 | var vim_session := vim.current 1582 | var input_state := vim_session.input_state 1583 | 1584 | # We respecte selection start position if we are in visual mod 1585 | var start := vim_session.visual_start_pos if vim_session.visual_mode else Position.new(ed.curr_line(), ed.curr_column()) 1586 | 1587 | # If there is an operator pending, then we do need a motion or operator (for linewise operation) 1588 | if not input_state.operator.is_empty() and (command.type != MOTION and command.type != OPERATOR): 1589 | return false 1590 | 1591 | if command.type == ACTION: 1592 | var action_args = command.get("action_args", {}) 1593 | if command.keys[-1] == "{char}": 1594 | action_args.selected_character = char(input_state.buffer.back().unicode) 1595 | process_action(command.action, action_args, ed, vim) 1596 | return true 1597 | elif command.type == MOTION or command.type == OPERATOR_MOTION: 1598 | var motion_args = command.get("motion_args", {}) 1599 | 1600 | if command.type == OPERATOR_MOTION: 1601 | var operator_args = command.get("operator_args", {}) 1602 | operator_args.original_pos = start 1603 | 1604 | input_state.operator = command.operator 1605 | input_state.operator_args = operator_args 1606 | 1607 | if command.keys[-1] == "{char}": 1608 | motion_args.selected_character = char(input_state.buffer.back().unicode) 1609 | 1610 | # Handle the motion and get the new cursor position 1611 | var new_pos = process_motion(command.motion, motion_args, ed, vim) 1612 | if new_pos == null: 1613 | return true 1614 | 1615 | # In some cases (text object), we need to override the start position 1616 | if new_pos is TextRange: 1617 | start = new_pos.from 1618 | new_pos = new_pos.to 1619 | 1620 | var inclusive : bool = motion_args.get("inclusive", false) 1621 | var jump_forward := start.compares_to(new_pos) < 0 1622 | 1623 | if vim_session.visual_mode: # Visual mode 1624 | if vim_session.visual_line: 1625 | if jump_forward: 1626 | ed.select(start.line, 0, new_pos.line, INF_COL) 1627 | else: 1628 | ed.select(start.line, INF_COL, new_pos.line, -1) 1629 | else: 1630 | ed.select_by_pos2(start, new_pos) 1631 | else: # Normal mode 1632 | if input_state.operator.is_empty(): # Motion only 1633 | ed.jump_to(new_pos.line, new_pos.column) 1634 | else: # Operator motion 1635 | # Check if we need to exlude the last character in selection 1636 | if not inclusive: 1637 | if jump_forward: 1638 | new_pos = new_pos.left() 1639 | else: 1640 | start = start.left() 1641 | 1642 | ed.select_by_pos2(start, new_pos) 1643 | process_operator(input_state.operator, input_state.operator_args, ed, vim) 1644 | return true 1645 | elif command.type == OPERATOR: 1646 | var operator_args = command.get("operator_args", {}) 1647 | operator_args.original_pos = start 1648 | 1649 | if vim.current.visual_mode: 1650 | operator_args.line_wise = vim.current.visual_line 1651 | process_operator(command.operator, operator_args, ed, vim) 1652 | vim.current.enter_normal_mode() 1653 | return true 1654 | 1655 | # Otherwise, we are in normal mode 1656 | if input_state.operator.is_empty(): # We are not fully done yet, need to wait for the motion 1657 | input_state.operator = command.operator 1658 | input_state.operator_args = operator_args 1659 | input_state.buffer.clear() 1660 | return false 1661 | 1662 | # Line wise operation 1663 | if input_state.operator == command.operator: 1664 | operator_args.line_wise = true 1665 | var new_pos : Position = process_motion("expand_to_line", {}, ed, vim) 1666 | ed.select(start.line, 0, new_pos.line, new_pos.column) 1667 | process_operator(command.operator, operator_args, ed, vim) 1668 | 1669 | return true 1670 | 1671 | return false 1672 | 1673 | func process_action(action: String, action_args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 1674 | if DEBUGGING: 1675 | print(" Action: %s %s" % [action, action_args]) 1676 | 1677 | action_args.repeat = max(1, vim.current.input_state.get_repeat()) 1678 | Callable(Command, action).call(action_args, ed, vim) 1679 | 1680 | if vim.current.visual_mode and action != "enter_visual_mode": 1681 | vim.current.enter_normal_mode() 1682 | 1683 | func process_operator(operator: String, operator_args: Dictionary, ed: EditorAdaptor, vim: Vim) -> void: 1684 | if DEBUGGING: 1685 | print(" Operator %s %s on %s" % [operator, operator_args, ed.selection()]) 1686 | 1687 | # Perform operation 1688 | Callable(Command, operator).call(operator_args, ed, vim) 1689 | 1690 | if operator_args.get("maintain_position", false): 1691 | var original_pos = operator_args.get("original_pos") 1692 | ed.jump_to(original_pos.line, original_pos.column) 1693 | 1694 | func process_motion(motion: String, motion_args: Dictionary, ed: EditorAdaptor, vim: Vim) -> Variant: 1695 | # Get current position 1696 | var cur := Position.new(ed.curr_line(), ed.curr_column()) 1697 | 1698 | # In Godot 4.3, CodeEdit.select moves cursor as well. If we select forward, the cursor will be positioned at the next column of the last selected column. 1699 | # But for VIM in the same case, the cursor position is exactly the last selected column. So we move back by one column when we considering the current position. 1700 | if vim.current.visual_mode: 1701 | if ed.code_editor.get_selection_origin_column() < ed.code_editor.get_caret_column(): 1702 | cur.column -= 1 1703 | 1704 | # Prepare motion args 1705 | var user_repeat = vim.current.input_state.get_repeat() 1706 | if user_repeat > 0: 1707 | motion_args.repeat = user_repeat 1708 | motion_args.repeat_is_explicit = true 1709 | else: 1710 | motion_args.repeat = 1 1711 | motion_args.repeat_is_explicit = false 1712 | 1713 | # Calculate new position 1714 | var result = Callable(Command, motion).call(cur, motion_args, ed, vim) 1715 | if result is Position: 1716 | var new_pos : Position = result 1717 | if new_pos.column == INF_COL: # INF_COL means the last column 1718 | new_pos.column = ed.last_column(new_pos.line) 1719 | 1720 | if motion_args.get('to_jump_list', false): 1721 | vim.current.jump_list.add(cur, new_pos) 1722 | 1723 | # Save last motion 1724 | vim.current.last_motion = motion 1725 | 1726 | if DEBUGGING: 1727 | print(" Motion: %s %s to %s" % [motion, motion_args, result]) 1728 | 1729 | return result 1730 | -------------------------------------------------------------------------------- /addons/godot-vim/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 53 | 57 | 63 | 69 | 75 | 81 | 87 | 93 | 99 | 105 | 106 | 107 | 110 | 113 | 118 | 123 | 128 | 133 | 138 | 143 | 148 | 153 | 158 | 163 | 166 | 171 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /addons/godot-vim/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="godot-vim" 4 | description="VIM bindings for godot4" 5 | author="Original: Josh N; Forked by Wenqiang Wang" 6 | version="4.3.0" 7 | script="godot-vim.gd" 8 | --------------------------------------------------------------------------------