└── addons └── nhb_functions_on_the_fly ├── plugin.gd.uid ├── lib ├── utils.gd.uid ├── class-finder.gd.uid ├── class-finder.gd └── utils.gd ├── icon.png ├── plugin.cfg ├── icon.png.import ├── LICENSE.md ├── README.md └── plugin.gd /addons/nhb_functions_on_the_fly/plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://5fmgdtrj8r3j 2 | -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/lib/utils.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dct7x5aw6nygl 2 | -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/lib/class-finder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c8db7kbdv62j0 2 | -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickHatBoecker/nhb_functions_on_the_fly/HEAD/addons/nhb_functions_on_the_fly/icon.png -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="NhbCreateFunctionsOnTheFly" 4 | description="With this plugin you can auto generate functions and get/set variables by selecting the function's / variable's name." 5 | author="NickHatBoecker" 6 | version="1.0.5" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://wh4gldabavoq" 6 | path="res://.godot/imported/icon.png-76880353bb37aa9ef87ad1674fafb417.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/nhb_functions_on_the_fly/icon.png" 14 | dest_files=["res://.godot/imported/icon.png-76880353bb37aa9ef87ad1674fafb417.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 NickHatBoecker 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/lib/class-finder.gd: -------------------------------------------------------------------------------- 1 | ## This script will find a custom class based on its class_name. 2 | ## If a script was found, its script path will be returned. 3 | ## If no script is found within {CANCEL_AFTER_MS} ms, an empty string is returned. 4 | class_name NhbFunctionsOnTheFlyClassFinder 5 | extends Node 6 | 7 | 8 | const CANCEL_AFTER_MS = 3000 9 | 10 | 11 | var _found_path: String 12 | 13 | 14 | func get_found_path() -> String: 15 | return _found_path 16 | 17 | 18 | func find_class_path(name_of_class : String) -> String: 19 | var dir = DirAccess.open("res://") 20 | if not dir: 21 | return "" 22 | 23 | var start_ms = Time.get_ticks_msec() 24 | _search_dir(dir, name_of_class, start_ms) 25 | 26 | return _found_path 27 | 28 | 29 | func _search_dir(dir : DirAccess, target : String, start_ms : int): 30 | if Time.get_ticks_msec() - start_ms > 3000: 31 | _found_path = "" 32 | return 33 | 34 | for item in dir.get_files(): 35 | if !item.ends_with(".gd"): continue 36 | 37 | var path = dir.get_current_dir() + "/" + item 38 | var file = FileAccess.open(path, FileAccess.READ) 39 | if !file: 40 | continue 41 | 42 | while not file.eof_reached(): 43 | if Time.get_ticks_msec() - start_ms > CANCEL_AFTER_MS: 44 | _found_path = "" 45 | return 46 | var line = file.get_line() 47 | if line.begins_with("class_name "): 48 | var parts = line.split(" ") 49 | if parts.size() > 1 and parts[1] == target: 50 | _found_path = path 51 | return 52 | file.close() 53 | 54 | for sub in dir.get_directories(): 55 | dir.change_dir(sub) 56 | _search_dir(dir, target, start_ms) 57 | 58 | if _found_path != "": return 59 | 60 | dir.change_dir("..") 61 | -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/README.md: -------------------------------------------------------------------------------- 1 | # NHB Functions On The Fly for Godot 4.4+ 2 | 3 | Powered by Godot Report Issue 4 | Support me on Ko-fi 5 | 6 | 7 | Easily create missing functions or getter/setters for variables in Godot on the fly.\ 8 | You can install it via the Asset Library or [downloading a copy](https://github.com/nickhatboecker/nhb_functions_on_the_fly/archive/refs/heads/main.zip) from GitHub. 9 | 10 | ✨ Even function arguments and return types are considered, for both native and custom methods. ✨ 11 | 12 | Shortcuts are configurable in the Editor settings. Under "_Plugin > NHB Functions On The Fly_" 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 26 | 29 | 30 | 31 |
Create function Ctrl + [ 18 | Create getter/setter variable Ctrl + ' 19 |
24 | Screenshot: Create function 25 | 27 | Screenshot: Create getter/setter variable 28 |
32 | 33 | ## ❓ How to use 34 | 35 | ### Create function 36 | 37 | 1. Write `my_button.pressed.connect(on_button_pressed)` 38 | 2. Select `on_button_pressed` or put cursor on it 39 | 3. Now you can either 40 | - Right click > "Create function" 41 | - Ctrl + [ 42 | - ⌘ Command + [ (Mac) 43 | 4. Function arguments and return type (if any, based on variable/signal signature) will be considered. 44 | 45 | ### Create getter/setter for variable 46 | 47 | 1. Write `var my_var` or `var my_var: String` or `var my_var: String = "Hello world"` 48 | 2. Select `my_var` or put cursor on it 49 | 3. Now you can either 50 | - Right click > "Create get/set variable" 51 | - Ctrl + ' 52 | - ⌘ Command + ' (Mac) 53 | 4. Return type (if any) will be considered 54 | 55 | ## ⭐ Contributors 56 | 57 | - [Initial idea](https://www.reddit.com/r/godot/comments/1morndn/im_a_lazy_programmer_and_added_a_generate_code/) and get/set variable creation: [u/siwoku](https://www.reddit.com/user/siwoku/) 58 | - Get text under cursor, so you don't have to select the text: [u/newold25](https://www.reddit.com/user/newold25/) 59 | - Maintainer, considering indentation type, adding shorcuts: [u/NickHatBoecker](https://nickhatboecker.de/linktree/) 60 | 61 | Pleae feel free to create a pull request! 62 | -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/plugin.gd: -------------------------------------------------------------------------------- 1 | ## Idea by u/siwoku 2 | ## 3 | ## Contributors: 4 | ## - You don't have to select text to create a function u/newold25 5 | ## - Consider editor settings's indentation type u/NickHatBoecker 6 | ## - Added shortcuts (configurable in editor settings) u/NickHatBoecker 7 | 8 | @tool 9 | class_name NhbFunctionsOnTheFly 10 | extends EditorPlugin 11 | 12 | ## Editor setting for the function shortcut 13 | const FUNCTION_SHORTCUT: StringName = "function_shortcut" 14 | ## Editor setting for the get/set variable shortcut 15 | const GET_SET_SHORTCUT: StringName = "get_set_shortcut" 16 | 17 | const DEFAULT_SHORTCUT_FUNCTION = KEY_BRACKETLEFT 18 | const DEFAULT_SHORTCUT_GET_SET = KEY_APOSTROPHE 19 | 20 | ## If the current text matches this expression, the function popup menu item will be displayed. 21 | const FUNCTION_NAME_REGEX = "^[a-zA-Z_][a-zA-Z0-9_]*$" 22 | 23 | ## If the current text matches this expression, the variable popup menu item will be displayed. 24 | ## Must contain the keyword "var". 25 | const VARIABLE_NAME_REGEX = "var [a-zA-Z_][a-zA-Z0-9_]*" 26 | 27 | ## This is used to determine if a variable string already has a return type. 28 | const VARIABLE_RETURN_TYPE_REGEX = VARIABLE_NAME_REGEX + " *(?:: *([a-zA-Z_][a-zA-Z0-9_]*))?" 29 | 30 | const CALLBACK_MENU_PRIORITY = 1500 31 | enum CALLBACK_TYPES { FUNCTION, VARIABLE } 32 | 33 | var utils: NhbFunctionsOnTheFlyUtils 34 | var script_editor: ScriptEditor 35 | var current_popup: PopupMenu 36 | 37 | var function_shortcut: Shortcut 38 | var get_set_shortcut: Shortcut 39 | 40 | 41 | func _enter_tree(): 42 | utils = NhbFunctionsOnTheFlyUtils.new() 43 | script_editor = EditorInterface.get_script_editor() 44 | script_editor.connect("editor_script_changed", _on_script_changed) 45 | _setup_current_script() 46 | _init_shortcuts() 47 | 48 | 49 | func _exit_tree(): 50 | if script_editor and script_editor.is_connected("editor_script_changed", _on_script_changed): 51 | script_editor.disconnect("editor_script_changed", _on_script_changed) 52 | _cleanup_current_script() 53 | 54 | 55 | func _on_script_changed(_script): 56 | _setup_current_script() 57 | 58 | 59 | func _setup_current_script(): 60 | _cleanup_current_script() 61 | var current_editor = script_editor.get_current_editor() 62 | if current_editor: 63 | var code_edit = _find_code_edit(current_editor) 64 | if code_edit: 65 | current_popup = _find_popup_menu(current_editor) 66 | if current_popup: 67 | current_popup.connect("about_to_popup", _on_popup_about_to_show) 68 | 69 | 70 | func _cleanup_current_script(): 71 | if current_popup and current_popup.is_connected("about_to_popup", _on_popup_about_to_show): 72 | current_popup.disconnect("about_to_popup", _on_popup_about_to_show) 73 | current_popup = null 74 | 75 | 76 | func _find_code_edit(node: Node) -> CodeEdit: 77 | if node is CodeEdit: 78 | return node 79 | for child in node.get_children(): 80 | var result = _find_code_edit(child) 81 | if result: 82 | return result 83 | return null 84 | 85 | 86 | func _find_popup_menu(node: Node) -> PopupMenu: 87 | if node is PopupMenu: 88 | return node 89 | for child in node.get_children(): 90 | var result = _find_popup_menu(child) 91 | if result: 92 | return result 93 | return null 94 | 95 | 96 | func _on_popup_about_to_show(): 97 | var current_editor = script_editor.get_current_editor() 98 | if not current_editor: 99 | return 100 | 101 | var code_edit = _find_code_edit(current_editor) 102 | if not code_edit: 103 | return 104 | 105 | var selected_text = _get_selected_text(code_edit) 106 | if selected_text.is_empty(): 107 | return 108 | 109 | var current_line = utils.get_current_line_text(code_edit) 110 | 111 | ## Because variable regex is more precise, it has to be checked first 112 | if utils.should_show_create_variable(code_edit, current_line, VARIABLE_NAME_REGEX, _get_editor_settings()): 113 | _create_menu_item( 114 | "Create getter/setter for variable: " + selected_text, 115 | selected_text, 116 | code_edit, 117 | CALLBACK_TYPES.VARIABLE 118 | ) 119 | elif _should_show_create_function(code_edit, selected_text): 120 | _create_menu_item( 121 | "Create function: " + selected_text, 122 | selected_text, 123 | code_edit, 124 | CALLBACK_TYPES.FUNCTION 125 | ) 126 | 127 | 128 | func _create_menu_item(item_text: String, selected_text: String, code_edit: CodeEdit, callback_type: CALLBACK_TYPES) -> void: 129 | current_popup.add_separator() 130 | current_popup.add_item(item_text, CALLBACK_MENU_PRIORITY) 131 | 132 | if current_popup.is_connected("id_pressed", _on_menu_item_pressed): 133 | current_popup.disconnect("id_pressed", _on_menu_item_pressed) 134 | 135 | current_popup.connect("id_pressed", _on_menu_item_pressed.bind(selected_text, code_edit, CALLBACK_MENU_PRIORITY, callback_type)) 136 | 137 | 138 | func _should_show_create_function(code_edit: CodeEdit, text: String) -> bool: 139 | var script_text = code_edit.text 140 | 141 | ## Check if valid function name 142 | var regex = RegEx.new() 143 | regex.compile(FUNCTION_NAME_REGEX) 144 | if not regex.search(text): 145 | return false 146 | 147 | if _function_exists_anywhere(script_text, text): 148 | return false 149 | 150 | if _global_variable_exists(script_text, text): 151 | return false 152 | 153 | if _local_variable_exists_in_current_function(code_edit, text): 154 | return false 155 | 156 | if utils.is_in_comment(code_edit, text): 157 | return false 158 | 159 | return true 160 | 161 | 162 | func _function_exists_anywhere(script_text: String, func_name: String) -> bool: 163 | var regex = RegEx.new() 164 | regex.compile("func\\s+" + func_name + "\\s*\\(") 165 | return regex.search(script_text) != null 166 | 167 | 168 | func _global_variable_exists(script_text: String, var_name: String) -> bool: 169 | var lines = script_text.split("\n") 170 | var inside_function = false 171 | 172 | for i in range(lines.size()): 173 | var line = lines[i] 174 | var trimmed = line.strip_edges() 175 | 176 | if trimmed.begins_with("func "): 177 | inside_function = true 178 | continue 179 | 180 | if not inside_function: 181 | var regex = RegEx.new() 182 | regex.compile("^var\\s+" + var_name + "\\b") 183 | if regex.search(trimmed): 184 | return true 185 | else: 186 | if trimmed.is_empty(): 187 | continue 188 | elif not line.begins_with("\t") and not line.begins_with(" ") and trimmed != "": 189 | inside_function = false 190 | var regex = RegEx.new() 191 | regex.compile("^var\\s+" + var_name + "\\b") 192 | if regex.search(trimmed): 193 | return true 194 | 195 | return false 196 | 197 | 198 | func _local_variable_exists_in_current_function(code_edit: CodeEdit, var_name: String) -> bool: 199 | var script_text = code_edit.text 200 | var current_line = code_edit.get_caret_line() 201 | 202 | var function_start = -1 203 | var function_end = -1 204 | var lines = script_text.split("\n") 205 | 206 | for i in range(current_line, -1, -1): 207 | if i < lines.size(): 208 | var line = lines[i].strip_edges() 209 | if line.begins_with("func "): 210 | function_start = i 211 | break 212 | 213 | if function_start == -1: 214 | return false 215 | 216 | for i in range(function_start + 1, lines.size()): 217 | var line = lines[i].strip_edges() 218 | if line.begins_with("func ") or (line != "" and not lines[i].begins_with("\t") and not lines[i].begins_with(" ")): 219 | function_end = i - 1 220 | break 221 | 222 | if function_end == -1: 223 | function_end = lines.size() - 1 224 | 225 | for i in range(function_start, function_end + 1): 226 | if i < lines.size(): 227 | var line = lines[i].strip_edges() 228 | 229 | var regex = RegEx.new() 230 | regex.compile("^var\\s+" + var_name + "\\b") 231 | if regex.search(line): 232 | return true 233 | 234 | if i == function_start: 235 | var param_regex = RegEx.new() 236 | param_regex.compile("func\\s+\\w+\\s*\\([^)]*\\b" + var_name + "\\b") 237 | if param_regex.search(line): 238 | return true 239 | 240 | return false 241 | 242 | 243 | func _on_menu_item_pressed(id: int, original_text: String, code_edit: CodeEdit, target_index: int, callback_type: CALLBACK_TYPES) -> void: 244 | if id != target_index: return 245 | 246 | if callback_type == CALLBACK_TYPES.FUNCTION: 247 | utils.create_function(original_text, code_edit, _get_editor_settings()) 248 | elif callback_type == CALLBACK_TYPES.VARIABLE: 249 | utils.create_get_set_variable(original_text, code_edit, VARIABLE_RETURN_TYPE_REGEX, _get_editor_settings()) 250 | 251 | 252 | ## Process the user defined shortcuts 253 | func _shortcut_input(event: InputEvent) -> void: 254 | if !event.is_pressed() || event.is_echo(): 255 | return 256 | 257 | if function_shortcut.matches_event(event): 258 | ## Function 259 | get_viewport().set_input_as_handled() 260 | var code_edit: CodeEdit = _get_code_edit() 261 | var function_name = utils.get_word_under_cursor(code_edit) 262 | if _should_show_create_function(code_edit, function_name): 263 | utils.create_function(function_name, code_edit, _get_editor_settings()) 264 | elif get_set_shortcut.matches_event(event): 265 | ## Get/set variable 266 | get_viewport().set_input_as_handled() 267 | var code_edit: CodeEdit = _get_code_edit() 268 | var variable_name = utils.get_word_under_cursor(code_edit) 269 | utils.create_get_set_variable(variable_name, code_edit, VARIABLE_RETURN_TYPE_REGEX, _get_editor_settings()) 270 | 271 | 272 | func _get_editor_settings() -> EditorSettings: 273 | return EditorInterface.get_editor_settings() 274 | 275 | 276 | ## Initializes all shortcuts. 277 | ## Every shortcut can be changed while this plugin is active, which will override them. 278 | func _init_shortcuts(): 279 | var editor_settings: EditorSettings = _get_editor_settings() 280 | 281 | if !editor_settings.has_setting(utils.get_shortcut_path(FUNCTION_SHORTCUT)): 282 | var shortcut: Shortcut = Shortcut.new() 283 | var event: InputEventKey = InputEventKey.new() 284 | event.device = -1 285 | event.command_or_control_autoremap = true 286 | event.keycode = DEFAULT_SHORTCUT_FUNCTION 287 | 288 | shortcut.events = [ event ] 289 | editor_settings.set_setting(utils.get_shortcut_path(FUNCTION_SHORTCUT), shortcut) 290 | 291 | if !editor_settings.has_setting(utils.get_shortcut_path(GET_SET_SHORTCUT)): 292 | var shortcut: Shortcut = Shortcut.new() 293 | var event: InputEventKey = InputEventKey.new() 294 | event.device = -1 295 | event.command_or_control_autoremap = true 296 | event.keycode = DEFAULT_SHORTCUT_GET_SET 297 | 298 | shortcut.events = [ event ] 299 | editor_settings.set_setting(utils.get_shortcut_path(GET_SET_SHORTCUT), shortcut) 300 | 301 | function_shortcut = editor_settings.get_setting(utils.get_shortcut_path(FUNCTION_SHORTCUT)) 302 | get_set_shortcut = editor_settings.get_setting(utils.get_shortcut_path(GET_SET_SHORTCUT)) 303 | 304 | 305 | ## This is the editor window, where code lines can be selected. 306 | func _get_code_edit() -> CodeEdit: 307 | return get_editor_interface().get_script_editor().get_current_editor().get_base_editor() 308 | 309 | 310 | func _get_selected_text(_code_edit: CodeEdit) -> String: 311 | var selected_text = _code_edit.get_selected_text().strip_edges() 312 | 313 | if selected_text.is_empty(): 314 | selected_text = utils.get_word_under_cursor(_code_edit) 315 | 316 | return selected_text 317 | -------------------------------------------------------------------------------- /addons/nhb_functions_on_the_fly/lib/utils.gd: -------------------------------------------------------------------------------- 1 | ## All functions of this script are testable. 2 | 3 | class_name NhbFunctionsOnTheFlyUtils 4 | extends Node 5 | 6 | 7 | enum INDENTATION_TYPES { TABS, SPACES } 8 | 9 | 10 | func is_in_comment(code_edit: CodeEdit, selected_text: String) -> bool: 11 | if selected_text.begins_with("#"): return true 12 | 13 | var caret_line = code_edit.get_caret_line() 14 | var line_text = code_edit.get_line(caret_line) 15 | var selection_start = code_edit.get_selection_from_column() 16 | 17 | var comment_pos = line_text.find("#") 18 | if comment_pos != -1 and comment_pos < selection_start: 19 | return true 20 | 21 | return false 22 | 23 | 24 | ## Returns true if 25 | ## - text contains a valid variable syntax 26 | ## - and text is not a comment line 27 | ## 28 | ## @param `text` contains the whole line of code. 29 | func should_show_create_variable(code_edit: CodeEdit, text: String, variable_name_regex: String, settings) -> bool: 30 | ## Check if valid variable name 31 | var regex = RegEx.new() 32 | regex.compile(variable_name_regex) 33 | if not regex.search(text): 34 | return false 35 | 36 | if is_in_comment(code_edit, text): 37 | return false 38 | 39 | if !is_global_variable(text, settings): 40 | return false 41 | 42 | return true 43 | 44 | 45 | ## Return return type or an empty string if no return type is provided. 46 | ## @example `var button: Button` will return "Button" 47 | ## @example `var button` will return "" 48 | func get_variable_return_type(text: String, variable_return_type_regex: String) -> String: 49 | var regex = RegEx.new() 50 | regex.compile(variable_return_type_regex) 51 | 52 | var result = regex.search(text) 53 | if not result: 54 | return "" 55 | 56 | return result.get_string(1) 57 | 58 | 59 | func get_current_line_text(_code_edit: CodeEdit) -> String: 60 | return _code_edit.get_line(_code_edit.get_caret_line()) 61 | 62 | 63 | func get_shortcut_path(parameter: String) -> String: 64 | return "res://addons/nhb_functions_on_the_fly/%s" % parameter 65 | 66 | 67 | ## Get accumulated indentation string. 68 | ## Will return "\t" if tabs are used for indentation. 69 | ## Will return " " * indent/size if spaces are used for indentation. 70 | func get_indentation_character(settings) -> String: 71 | var indentation_type = settings.get_setting("text_editor/behavior/indent/type") 72 | var indentation_character: String = "\t" 73 | 74 | if indentation_type != INDENTATION_TYPES.TABS: 75 | var indentation_size = settings.get_setting("text_editor/behavior/indent/size") 76 | indentation_character = " ".repeat(indentation_size) 77 | 78 | return indentation_character 79 | 80 | 81 | func create_get_set_variable(variable_name: String, code_edit: CodeEdit, variable_return_type_regex: String, settings) -> void: 82 | var current_line : int = code_edit.get_caret_line() 83 | var line_text : String = code_edit.get_line(current_line) 84 | 85 | if !is_global_variable(line_text, settings): return 86 | 87 | var end_column : int = line_text.length() 88 | var indentation_character: String = get_indentation_character(settings) 89 | 90 | var return_type: String = ": Variant" 91 | if not get_variable_return_type(line_text, variable_return_type_regex).is_empty(): 92 | ## Variable already has a return type. 93 | return_type = "" 94 | if line_text.contains("="): 95 | ## Variable has a value so omit return type. 96 | return_type = "" 97 | 98 | var code_text: String = "%s:\n%sget:\n%sreturn %s\n%sset(value):\n%s%s = value" % [ 99 | return_type, 100 | indentation_character, 101 | indentation_character.repeat(2), 102 | variable_name, 103 | indentation_character, 104 | indentation_character.repeat(2), 105 | variable_name 106 | ] 107 | 108 | code_edit.deselect() 109 | code_edit.insert_text(code_text, current_line, end_column) 110 | 111 | 112 | func create_function(function_name: String, code_edit: CodeEdit, settings): 113 | var current_line : int = code_edit.get_caret_line() 114 | var line_text : String = code_edit.get_line(current_line) 115 | 116 | code_edit.deselect() 117 | 118 | var return_type: String = get_function_return_type(function_name, code_edit) 119 | var return_type_string: String = " -> %s" % return_type 120 | if !return_type: 121 | return_type_string = "" 122 | 123 | var return_value: String = get_return_value_by_return_type(return_type) 124 | var return_value_string: String = " %s" % str(return_value) 125 | if !return_value: 126 | return_value_string = "" 127 | 128 | var function_parameters = find_signal_declaration_parameters(code_edit.get_caret_line(), line_text, code_edit) 129 | if !function_parameters: 130 | function_parameters = find_function_parameters(function_name, code_edit.get_caret_line(), line_text, code_edit) 131 | 132 | var indentation_character: String = get_indentation_character(settings) 133 | var new_function = "\n\nfunc %s(%s)%s:\n%sreturn%s" % [ 134 | function_name, 135 | function_parameters, 136 | return_type_string, 137 | indentation_character, 138 | return_value_string 139 | ] 140 | 141 | code_edit.text = code_edit.text + new_function 142 | 143 | var line_with_new_function = code_edit.get_line_count() - 1 144 | code_edit.set_caret_line(line_with_new_function) 145 | code_edit.set_caret_column(code_edit.get_line(line_with_new_function).length()) 146 | 147 | code_edit.text_changed.emit() 148 | 149 | 150 | ## @TODO Is there any easier way to create default value based on return type? 151 | func get_return_value_by_return_type(return_type: String) -> String: 152 | match (return_type): 153 | "String": 154 | return "\"\"" 155 | "int": 156 | return "0" 157 | "float": 158 | return "0.0" 159 | "bool": 160 | return "false" 161 | "Color": 162 | return "Color.WHITE" 163 | "Array": 164 | return "[]" 165 | "Dictionary": 166 | return "{}" 167 | "Vector2": 168 | return "Vector2.ZERO" 169 | "Vector2i": 170 | return "Vector2i.ZERO" 171 | "Vector3": 172 | return "Vector3.ZERO" 173 | "Vector3i": 174 | return "Vector3i.ZERO" 175 | "Vector4": 176 | return "Vector4.ZERO" 177 | "Vector4i": 178 | return "Vector4i.ZERO" 179 | return "" 180 | 181 | 182 | func get_word_under_cursor(code_edit: CodeEdit) -> String: 183 | var caret_line = code_edit.get_caret_line() 184 | var caret_column = code_edit.get_caret_column() 185 | var line_text = code_edit.get_line(caret_line) 186 | 187 | var start = caret_column 188 | while start > 0 and line_text[start - 1].is_subsequence_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"): 189 | start -= 1 190 | 191 | var end = caret_column 192 | while end < line_text.length() and line_text[end].is_subsequence_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"): 193 | end += 1 194 | 195 | return line_text.substr(start, end - start) 196 | 197 | 198 | func trim_char(text: String, char: String) -> String: 199 | if text.is_empty() or char.is_empty(): 200 | return text 201 | 202 | var start := 0 203 | var end := text.length() 204 | 205 | while start < end and text.substr(start, char.length()) == char: 206 | start += char.length() 207 | 208 | while end > start and text.substr(end - char.length(), char.length()) == char: 209 | end -= char.length() 210 | 211 | return text.substr(start, end - start) 212 | 213 | 214 | func get_variable_from_line(line_index: int, line_text : String, code_edit: CodeEdit) -> String: 215 | var parts = line_text.split("=") 216 | if parts.size() < 2: 217 | parts = line_text.split(".") 218 | if parts.size() < 2: 219 | return "" 220 | 221 | var left_part = parts[0].strip_edges() 222 | 223 | var name_token = left_part.split(":")[0].strip_edges() 224 | name_token = name_token.split(".")[0].strip_edges() 225 | if name_token == "": 226 | return "" 227 | 228 | return name_token 229 | 230 | 231 | ## Check for variable declaration in current line and all lines above. 232 | func find_variable_declaration_return_type(variable_name: String, line_index: int, line_text : String, code_edit: CodeEdit) -> String: 233 | for i in range(line_index, -1, -1): 234 | var prev_line = code_edit.get_line(i).strip_edges() 235 | 236 | if !prev_line.begins_with("var") and !prev_line.begins_with("@onready") and !prev_line.begins_with("@export"): 237 | continue 238 | 239 | if variable_name not in prev_line: 240 | continue 241 | 242 | var rest = prev_line.split(variable_name)[1].strip_edges() 243 | if rest.begins_with(":"): 244 | var type_part = rest.split("=") 245 | type_part = type_part[0].split(":")[1] 246 | return type_part.strip_edges() 247 | 248 | return "" 249 | 250 | 251 | func find_signal_declaration_parameters(line_index: int, line_text : String, code_edit: CodeEdit) -> String: 252 | var variable_name: String = get_variable_from_line(line_index, line_text, code_edit) 253 | var object_name: String = find_variable_declaration_return_type(variable_name, line_index, line_text, code_edit) 254 | if !object_name: return "" 255 | 256 | var instance: Variant 257 | if type_exists(object_name): 258 | instance = ClassDB.instantiate(object_name) 259 | 260 | if !instance or !instance.has_method("get_signal_list"): 261 | var class_finder = NhbFunctionsOnTheFlyClassFinder.new() 262 | var script_path: String = class_finder.find_class_path(object_name) 263 | class_finder.free() 264 | if !script_path: return "" 265 | 266 | instance = load(script_path).new() 267 | 268 | var signal_name = get_signal_name_by_line(line_text) 269 | if !instance.has_signal(signal_name): return "" 270 | 271 | var signal_parameters = [] 272 | for signature in instance.get_signal_list(): 273 | if signature.name != signal_name: continue 274 | 275 | signal_parameters = signature.args 276 | 277 | if signal_parameters.size() == 0: 278 | instance.free() 279 | return "" 280 | 281 | var signal_parameter_string_parts: Array = [] 282 | for i: Dictionary in signal_parameters: 283 | if !i.has("class_name"): 284 | signal_parameter_string_parts.push_back(i.name) 285 | elif !i.class_name.is_empty(): 286 | signal_parameter_string_parts.push_back("%s: %s" % [i.name, i.class_name]) 287 | elif i.class_name.is_empty() and i.has("type"): 288 | signal_parameter_string_parts.push_back("%s: %s" % [i.name, type_string(i.type)]) 289 | 290 | instance.free() 291 | 292 | return ", ".join(signal_parameter_string_parts) 293 | 294 | 295 | func find_function_parameters(function_name: String, line_index: int, line_text : String, code_edit: CodeEdit) -> String: 296 | var regex = RegEx.new() 297 | regex.compile(function_name + "(\\(.*\\))") 298 | 299 | var result = regex.search(line_text) 300 | if not result: 301 | return "" 302 | 303 | var arguments = result.get_string(1).lstrip("(").rstrip(")").split(",", false) 304 | var argumentString: String 305 | 306 | for i in arguments.size(): 307 | arguments[i] = arguments[i].strip_edges() 308 | var argumentType = find_variable_declaration_return_type(arguments[i], line_index, line_text, code_edit) 309 | if argumentType: 310 | arguments[i] = "%s: %s" % [arguments[i], argumentType] 311 | 312 | return ", ".join(arguments) 313 | 314 | 315 | func get_signal_name_by_line(line_text: String) -> String: 316 | var parts = line_text.split(".connect") 317 | if parts.size() == 1: return "" 318 | 319 | parts = parts[0].split(".") 320 | if parts.size() == 1: return "" 321 | 322 | return parts[parts.size() - 1] 323 | 324 | 325 | func get_function_return_type(function_name: String, code_edit: CodeEdit): 326 | var current_line = get_current_line_text(code_edit).strip_edges() 327 | 328 | if current_line.begins_with(function_name): 329 | ## We cannot determine return type if function name is the only information in this line. 330 | return "" 331 | 332 | if current_line.contains("="): 333 | ## Function is tied to a variable. If this variable was initialized with a return type, we can use it. 334 | var variable_name: String = get_variable_from_line(code_edit.get_caret_line(), current_line, code_edit) 335 | return find_variable_declaration_return_type(variable_name, code_edit.get_caret_line(), current_line, code_edit) 336 | 337 | if current_line.contains(".connect"): 338 | return "void" 339 | 340 | return "" 341 | 342 | 343 | ## Lines with global variables do not start with whitespace. 344 | func is_global_variable(text: String, settings) -> bool: 345 | var indentation_character: String = get_indentation_character(settings) 346 | 347 | var regex = RegEx.new() 348 | regex.compile("^\\s") 349 | 350 | if not regex.search(text): 351 | return true 352 | 353 | return false 354 | --------------------------------------------------------------------------------