diff --git a/addons/GDQuest_GDScript_formatter/LICENSE b/addons/GDQuest_GDScript_formatter/LICENSE deleted file mode 100644 index d2053ea..0000000 --- a/addons/GDQuest_GDScript_formatter/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025-present GDQuest - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/addons/GDQuest_GDScript_formatter/install_and_update.gd b/addons/GDQuest_GDScript_formatter/install_and_update.gd deleted file mode 100644 index cd4a708..0000000 --- a/addons/GDQuest_GDScript_formatter/install_and_update.gd +++ /dev/null @@ -1,240 +0,0 @@ -@tool -extends Node - -enum HttpRequestState { - IDLE, - FETCHING_RELEASE_INFO, - DOWNLOADING_BINARY, -} - -const URL_GITHUB_API_LATEST_RELEASE = "https://api.github.com/repos/gdquest/GDScript-formatter/releases/latest" -signal installation_completed(binary_path: String) -signal installation_failed(error_message: String) - -var http_request_state := HttpRequestState.IDLE -var http_request: HTTPRequest = HTTPRequest.new() -var formatter_cache_dir: String - - -func _init(cache_dir: String) -> void: - formatter_cache_dir = cache_dir - - -func _ready() -> void: - add_child(http_request) - http_request.request_completed.connect(_on_request_completed) - - -func install_or_update_formatter() -> void: - print("Starting GDScript formatter installation...") - http_request_state = HttpRequestState.FETCHING_RELEASE_INFO - http_request.request(URL_GITHUB_API_LATEST_RELEASE) - - -func _on_request_completed( - _http_result: int, - response_code: int, - _http_headers: PackedStringArray, - body: PackedByteArray, -) -> void: - if response_code != 200: - var error_message := "HTTP request failed. Response code: " + str(response_code) - push_error(error_message) - http_request_state = HttpRequestState.IDLE - installation_failed.emit(error_message) - return - - match http_request_state: - HttpRequestState.FETCHING_RELEASE_INFO: - _process_response_latest_release(body) - HttpRequestState.DOWNLOADING_BINARY: - _process_response_download_file(body) - _: - var error_message := "Unexpected HTTP request state: " + str(http_request_state) - push_error(error_message) - http_request_state = HttpRequestState.IDLE - installation_failed.emit(error_message) - - -func _process_response_latest_release(body: PackedByteArray) -> void: - var json = JSON.parse_string(body.get_string_from_utf8()) - if not json or not json.has("assets"): - var error_message := "Failed to parse release information from GitHub API" - push_error(error_message) - http_request_state = HttpRequestState.IDLE - installation_failed.emit(error_message) - return - - print("GDScript Formatter release information loaded successfully") - - var assets = json["assets"] - var tag = json["tag_name"] - var download_url := _find_matching_asset(assets, tag) - - if download_url.is_empty(): - var error_message := "No matching binary found for current platform" - push_error(error_message) - http_request_state = HttpRequestState.IDLE - installation_failed.emit(error_message) - return - - print("Found compatible release, starting download...\n", "Download URL: ", download_url) - - http_request_state = HttpRequestState.DOWNLOADING_BINARY - http_request.request(download_url) - - -func _process_response_download_file(body: PackedByteArray) -> void: - http_request_state = HttpRequestState.IDLE - - print("Download completed successfully (", body.size(), " bytes)") - - var asset_info := _get_platform_info() - if asset_info.is_empty(): - var error_message := "Failed to determine platform information" - push_error(error_message) - installation_failed.emit(error_message) - return - - print("Extracting and installing binary...") - var binary_path := _download_and_install_binary(body, asset_info) - if binary_path.is_empty(): - var error_message := "Installation failed" - push_error(error_message) - installation_failed.emit(error_message) - return - - print( - "\n".join(["GDScript formatter installed successfully!", "Binary location: " + binary_path]), - ) - installation_completed.emit(binary_path) - - -func _get_platform_info() -> Dictionary: - var os_name := OS.get_name().to_lower() - var processor_name := OS.get_processor_name().to_lower() - var architecture := "x86_64" - - if ( - processor_name.contains("aarch64") or processor_name.contains("arm64") - or processor_name.contains("apple") - ): - architecture = "aarch64" - elif processor_name.contains("x86_64") or processor_name.contains("amd64"): - architecture = "x86_64" - - var binary_name := "gdscript-formatter" - if os_name.contains("windows"): - binary_name = "gdscript-formatter.exe" - - var platform_info := { "architecture": architecture, "binary_name": binary_name } - - if os_name.contains("windows"): - platform_info["os"] = "windows" - elif os_name.contains("linux"): - platform_info["os"] = "linux" - elif os_name.contains("macos") or os_name.contains("osx"): - platform_info["os"] = "macos" - - return platform_info - - -func _find_matching_asset(assets: Array, tag: String) -> String: - var platform_info := _get_platform_info() - if platform_info.is_empty(): - return "" - - var expected_pattern := "gdscript-formatter-%s-%s-%s" % [ - tag, - platform_info["os"], - platform_info["architecture"], - ] - if platform_info["os"] == "windows": - expected_pattern += ".exe" - expected_pattern += ".zip" - - for asset in assets: - var asset_name: String = asset["name"] - if asset_name == expected_pattern: - return asset["browser_download_url"] - - return "" - - -func _download_and_install_binary(zip_data: PackedByteArray, platform_info: Dictionary) -> String: - var binary_name: String = platform_info["binary_name"] - # We get the contents of the zip file as a byte array. we need to write - # it to a file to be able to read it. We use a temporary file for that. - var temp_archive_path := formatter_cache_dir.path_join("temp_formatter_archive.zip") - - print("Installing to cache directory: ", formatter_cache_dir) - - # Create the gdquest cache directory if it doesn't exist - if not DirAccess.dir_exists_absolute(formatter_cache_dir): - var dir_result := DirAccess.make_dir_recursive_absolute(formatter_cache_dir) - if dir_result != OK: - push_error("Failed to create cache directory: ", formatter_cache_dir) - return "" - - var temp_file := FileAccess.open(temp_archive_path, FileAccess.WRITE) - if not temp_file: - push_error("Failed to create temporary archive file") - return "" - - temp_file.store_buffer(zip_data) - temp_file.close() - - var zip_reader := ZIPReader.new() - var err := zip_reader.open(temp_archive_path) - if err != OK: - push_error("Failed to open ZIP archive from temporary file") - zip_reader.close() - DirAccess.remove_absolute(temp_archive_path) - return "" - - var files := zip_reader.get_files() - var binary_data: PackedByteArray - var found_binary := false - var actual_binary_name := "" - - for file_path in files: - if not file_path.ends_with("/"): - binary_data = zip_reader.read_file(file_path) - actual_binary_name = file_path.get_file() - found_binary = true - break - - zip_reader.close() - - DirAccess.remove_absolute(temp_archive_path) - if not found_binary: - push_error("No executable found in ZIP archive") - return "" - - var binary_path := formatter_cache_dir.path_join(binary_name) - - var file := FileAccess.open(binary_path, FileAccess.WRITE) - if not file: - push_error("Failed to create binary file at: ", binary_path) - return "" - - file.store_buffer(binary_data) - file.close() - - if not OS.get_name().to_lower().contains("windows"): - # This should be equivalent to setting the binary to permissions 755 - const UNIX_EXECUTABLE_PERMISSIONS = ( - FileAccess.UNIX_READ_OWNER | FileAccess.UNIX_WRITE_OWNER - | FileAccess.UNIX_EXECUTE_OWNER | FileAccess.UNIX_READ_GROUP - | FileAccess.UNIX_EXECUTE_GROUP | FileAccess.UNIX_READ_OTHER | FileAccess.UNIX_EXECUTE_OTHER - ) - var permissions_error := FileAccess.set_unix_permissions( - binary_path, - UNIX_EXECUTABLE_PERMISSIONS, - ) - if permissions_error != OK: - push_error("Failed to make formatter executable: ", binary_path) - DirAccess.remove_absolute(binary_path) - return "" - - return binary_path diff --git a/addons/GDQuest_GDScript_formatter/install_and_update.gd.uid b/addons/GDQuest_GDScript_formatter/install_and_update.gd.uid deleted file mode 100644 index 67fd1f5..0000000 --- a/addons/GDQuest_GDScript_formatter/install_and_update.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ccblhowfwltqi diff --git a/addons/GDQuest_GDScript_formatter/menu.gd b/addons/GDQuest_GDScript_formatter/menu.gd deleted file mode 100644 index 1818cf3..0000000 --- a/addons/GDQuest_GDScript_formatter/menu.gd +++ /dev/null @@ -1,166 +0,0 @@ -## This module handles adding a menu to the script editor with formatter commands. -## It safely locates the script editor's menu bar and adds our custom menu. -@tool -extends Node - -signal menu_item_selected(command: String) - -const MENU_TEXT = "Format" -const MENU_ITEMS = { - "format_script": "Format Current Script", - "lint_script": "Lint Current Script", - "reorder_code": "Reorder Code", - "install_update": "Install or Update Formatter", - "uninstall": "Uninstall Formatter", - "report_issue": "Report Issue", - "help": "Help", -} - -var menu_button: MenuButton = null -var popup_menu: PopupMenu = null - - -func _ready() -> void: - # At the start we insert the menu in the script editor - var script_editor := EditorInterface.get_script_editor() - var last_menu_button := _find_last_menu_button(script_editor) - if not is_instance_valid(last_menu_button): - push_warning( - "GDScript Formatter: Could not find valid menu button in script editor. Menu will not be available. Use the command palette instead." - ) - return - - menu_button = MenuButton.new() - menu_button.text = MENU_TEXT - menu_button.switch_on_hover = true - menu_button.flat = false - menu_button.theme_type_variation = &"FlatMenuButton" - - popup_menu = menu_button.get_popup() - _populate_menu() - - popup_menu.id_pressed.connect(_on_menu_item_pressed) - last_menu_button.add_sibling(menu_button) - - -## Cleans up the menu from the script editor. Call this when disabling the -## plugin (in _exit_tree()). -func remove_formatter_menu() -> void: - if is_instance_valid(menu_button): - if is_instance_valid(popup_menu): - popup_menu.id_pressed.disconnect(_on_menu_item_pressed) - menu_button.queue_free() - menu_button = null - popup_menu = null - - -func update_menu(show_uninstall: bool) -> void: - if not is_instance_valid(popup_menu): - return - popup_menu.clear() - _populate_menu(show_uninstall) - - -## Searches for and returns the last menu node in the script editor top menu bar, -## or null if not found. -func _find_last_menu_button(script_editor: Control) -> MenuButton: - # The first child of the script editor should be a VBoxContainer (main container for the script editor main screen) - # Then the first child of that container should be an HBoxContainer (the menu bar) - # This is based on the current structure of Godot's script editor as of Godot 4.5 - # Note: We add multiple checks in there with null returns mainly in case something changes in a future - if script_editor.get_child_count() == 0: - return null - - var main_container := script_editor.get_child(0) - if not is_instance_valid(main_container) or not main_container is VBoxContainer: - return null - - if main_container.get_child_count() == 0: - return null - - var menu_bar := main_container.get_child(0) - if not is_instance_valid(menu_bar) or not menu_bar is HBoxContainer: - return null - - # We reached the menu bar. So now we loop through all the children look for - # the last menu button, which would be the last menu in the menu list. - # Note: Here the goal is to insert the menu after the debug menu, but we - # don't check for the button text because "debug" would only work in English - # this should work in any language. - var last_menu_button: MenuButton = null - for child in menu_bar.get_children(): - if child is MenuButton: - last_menu_button = child as MenuButton - - return last_menu_button - - -func _populate_menu(show_uninstall: bool = true) -> void: - if not is_instance_valid(popup_menu): - return - - var current_item_index := 0 - - popup_menu.add_item(MENU_ITEMS["format_script"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "format_script") - popup_menu.set_item_tooltip( - current_item_index, - "Run the GDScript Formatter over the current script", - ) - - current_item_index += 1 - popup_menu.add_item(MENU_ITEMS["lint_script"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "lint_script") - popup_menu.set_item_tooltip(current_item_index, "Check the current script for linting issues") - - current_item_index += 1 - popup_menu.add_item(MENU_ITEMS["reorder_code"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "reorder_code") - popup_menu.set_item_tooltip( - current_item_index, - "Reorder the code elements in the current script according to the GDScript Style Guide", - ) - - popup_menu.add_separator() - - # NOTE: When we add separators, it bumps the internal index of menu items. - # That's why we have to increase it even on separators. Otherwise the - # tooltip will lose sync. - current_item_index += 2 - popup_menu.add_item(MENU_ITEMS["install_update"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "install_update") - popup_menu.set_item_tooltip( - current_item_index, - "Download the latest version of the GDScript Formatter", - ) - - if show_uninstall: - current_item_index += 1 - popup_menu.add_item(MENU_ITEMS["uninstall"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "uninstall") - popup_menu.set_item_tooltip( - current_item_index, - "Remove the GDScript Formatter installed through this add-on from your computer", - ) - - popup_menu.add_separator() - - # Bumped index by 1 extra step because of the previous separator. - current_item_index += 2 - popup_menu.add_item(MENU_ITEMS["report_issue"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "report_issue") - popup_menu.set_item_tooltip(current_item_index, "Tell us about problems or bugs you found") - - current_item_index += 1 - popup_menu.add_item(MENU_ITEMS["help"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "help") - popup_menu.set_item_tooltip(current_item_index, "Learn how to use the GDScript Formatter") - - -func _on_menu_item_pressed(id: int) -> void: - if not is_instance_valid(popup_menu): - return - - var command: String = popup_menu.get_item_metadata(id) - if not command.is_empty(): - menu_item_selected.emit(command) diff --git a/addons/GDQuest_GDScript_formatter/menu.gd.uid b/addons/GDQuest_GDScript_formatter/menu.gd.uid deleted file mode 100644 index d180571..0000000 --- a/addons/GDQuest_GDScript_formatter/menu.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bqwfn5bsuds1m diff --git a/addons/GDQuest_GDScript_formatter/plugin.cfg b/addons/GDQuest_GDScript_formatter/plugin.cfg deleted file mode 100644 index 51a43e0..0000000 --- a/addons/GDQuest_GDScript_formatter/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="GDQuest GDScript Formatter" -description="A plugin to format GDScript code in the Godot editor using the GDScript Formatter from GDQuest." -author="GDQuest" -version="0.25.0" -script="plugin.gd" diff --git a/addons/GDQuest_GDScript_formatter/plugin.gd b/addons/GDQuest_GDScript_formatter/plugin.gd deleted file mode 100644 index b143eb3..0000000 --- a/addons/GDQuest_GDScript_formatter/plugin.gd +++ /dev/null @@ -1,1080 +0,0 @@ -## This plug-in adds support for automatically formatting GDScript files on save -## and via a command in the Godot Editor, using the GDQuest GDScript Formatter. -## -## It also provides an option to install or update the formatter binary from the GitHub releases. -## -## See our website for more information on the formatter and how to use it: -## https://www.gdquest.com/library/gdscript_formatter/ -@tool -extends EditorPlugin - -const FormatterInstaller = preload("install_and_update.gd") -const FormatterMenu = preload("menu.gd") - -const EDITOR_SETTINGS_CATEGORY = "gdquest_gdscript_formatter/" -const SETTING_FORMAT_ON_SAVE = "format_on_save" -const SETTING_SHORTCUT = "shortcut" -const SETTING_USE_SPACES = "use_spaces" -const SETTING_INDENT_SIZE = "indent_size" -const SETTING_FORMAT_MODE = "format_mode" -const SETTING_REORDER_CODE = "reorder_code" -const SETTING_SAFE_MODE = "safe_mode" -const SETTING_FORMATTER_PATH = "formatter_path" -const SETTING_LINT_ON_SAVE = "lint_on_save" -const SETTING_LINT_LINE_LENGTH = "lint_line_length" -const SETTING_LINT_IGNORED_RULES = "lint_ignored_rules" -# Directories to ignore when Format on Save is enabled -const SETTING_IGNORED_DIRECTORIES = "format_on_save_ignored_directories" - -## Represents the modes the user wants to use by default, notably when running -## the formatter on save. -enum FormatMode { - NORMAL, - ## Reorder the code according to the official style guide every time the - ## formatter runs. Note that without this option, you can still reorder any - ## time from the format menu in the script editor. - REORDER_CODE, - ## Reparse the formatted code and compare its structure with the original. - ## - ## [b]WARNING:[/b] this is an imperfect check. It does not guarantee that - ## formatting is 100% safe or semantically equivalent. We always recommend - ## using a version control system when running the formatter. - ## - ## This is an option used notably for development of the formatter, when - ## testing it on new codebases, to quickly catch bugs on unsupported - ## GDScript syntax. - ## - ## When using the formatter normally on individual scripts, you can always - ## undo after formatting or use a version control system to track, review, - ## and undo changes. - VERIFY_STRUCTURE, -} - -const COMMAND_PALETTE_CATEGORY = "gdquest gdscript formatter/" -const COMMAND_PALETTE_FORMAT_SCRIPT = "Format GDScript" -const COMMAND_PALETTE_LINT_SCRIPT = "Lint GDScript" -const COMMAND_PALETTE_INSTALL_UPDATE = "Install or Update Formatter" -const COMMAND_PALETTE_UNINSTALL = "Uninstall Formatter" -const COMMAND_PALETTE_REPORT_ISSUE = "Report Issue" - -var DEFAULT_SETTINGS = { - SETTING_FORMAT_ON_SAVE: false, - SETTING_USE_SPACES: false, - SETTING_INDENT_SIZE: 4, - SETTING_FORMAT_MODE: FormatMode.NORMAL, - SETTING_FORMATTER_PATH: "", - SETTING_LINT_ON_SAVE: false, - SETTING_LINT_LINE_LENGTH: 100, - SETTING_LINT_IGNORED_RULES: "", - SETTING_IGNORED_DIRECTORIES: PackedStringArray(["addons/"]), -} - -## Which gutter lint icons are shown in. -## By default, gutter 0 is for breakpoints and 1 is for things like overrides. -const GUTTER_LINT_ICON_INDEX = 2 -const GUTTER_LINT_ICONS_NAME = "gdscript_formatter_lint_icons" - -var connection_list: Array[Resource] = [] -var installer: FormatterInstaller = null -var formatter_cache_dir: String -var menu: FormatterMenu = null -var _has_uninstall_command := false -var _has_formatter_command := false -var _has_format_command := false -var _has_lint_command := false -var _already_warned_about_reorder_on_save := false -var _already_warned_about_builtin_format_on_save := false -# Used to auto detect changes to the project's .editorconfig file. -var _editorconfig_last_modified_time := -1 -# Editorconfig allows setting rules per path glob. We track globs for the format -# on save rule here so users can enable it selectively for specific folders. -var _editorconfig_format_on_save_rules: Array[Dictionary] = [] - - -func _init() -> void: - migrate_format_mode_setting() - if not has_editor_setting(SETTING_FORMAT_MODE): - set_editor_setting(SETTING_FORMAT_MODE, DEFAULT_SETTINGS[SETTING_FORMAT_MODE]) - register_format_mode_setting() - - for setting: String in DEFAULT_SETTINGS.keys(): - if setting == SETTING_FORMAT_MODE: - continue - if not has_editor_setting(setting): - set_editor_setting(setting, DEFAULT_SETTINGS[setting]) - - if not has_editor_setting(SETTING_SHORTCUT): - var default_shortcut := InputEventKey.new() - default_shortcut.echo = false - default_shortcut.pressed = true - default_shortcut.keycode = KEY_I - default_shortcut.ctrl_pressed = true - default_shortcut.shift_pressed = false - default_shortcut.alt_pressed = true - - var shortcut := Shortcut.new() - shortcut.events.push_back(default_shortcut) - - set_editor_setting(SETTING_SHORTCUT, shortcut) - - -func register_format_mode_setting() -> void: - var editor_settings := EditorInterface.get_editor_settings() - var setting_name := EDITOR_SETTINGS_CATEGORY + SETTING_FORMAT_MODE - editor_settings.add_property_info( - { - "name": setting_name, - "type": TYPE_INT, - "hint": PROPERTY_HINT_ENUM, - "hint_string": "Normal,Reorder code,Verify structure", - } - ) - editor_settings.set_initial_value(setting_name, DEFAULT_SETTINGS[SETTING_FORMAT_MODE], false) - - -## Converts the old independent settings to the mutually exclusive format mode. -## The legacy safe mode takes priority because it is the least destructive option. -func migrate_format_mode_setting() -> void: - if has_editor_setting(SETTING_FORMAT_MODE): - return - - # Inferring a version number from the old settings; editor settings does not - # give us a neat way to version our settings so we do it manually. It's just - # to keep track of migrations. - var version := -1 - if has_editor_setting(SETTING_REORDER_CODE) or has_editor_setting(SETTING_SAFE_MODE): - version = 1 - - # Upgrade to version 2; that's when we merged safe mode and reorder code - # into a single format mode (because they're mutually exclusive). - if version == 1: - var format_mode := FormatMode.NORMAL - if has_editor_setting(SETTING_SAFE_MODE) and get_editor_setting(SETTING_SAFE_MODE) as bool: - format_mode = FormatMode.VERIFY_STRUCTURE - elif ( - has_editor_setting(SETTING_REORDER_CODE) - and get_editor_setting(SETTING_REORDER_CODE) as bool - ): - format_mode = FormatMode.REORDER_CODE - - set_editor_setting(SETTING_FORMAT_MODE, format_mode) - - # Remove the old settings so users do not see two conflicting configurations. - var editor_settings := EditorInterface.get_editor_settings() - for setting_name: String in [SETTING_REORDER_CODE, SETTING_SAFE_MODE]: - if has_editor_setting(setting_name): - editor_settings.erase(EDITOR_SETTINGS_CATEGORY + setting_name) - - version = 2 - - -func _enter_tree() -> void: - formatter_cache_dir = EditorInterface.get_editor_paths().get_cache_dir().path_join("gdquest") - installer = FormatterInstaller.new(formatter_cache_dir) - add_child(installer) - installer.installation_completed.connect( - func _on_installation_completed(binary_path: String) -> void: - set_editor_setting(SETTING_FORMATTER_PATH, binary_path) - _has_formatter_command = has_command(binary_path) - if not _has_formatter_command: - push_error( - "GDScript Formatter: Installed binary cannot be executed: " + binary_path - ) - return - add_format_command() - add_lint_command() - # After installing the formatter we can add the menu option to show the uninstall command - if is_instance_valid(menu): - menu.update_menu(true), - ) - installer.installation_failed.connect( - func _on_installation_failed(error_message: String) -> void: - push_error("Formatter installation failed: ", error_message), - ) - - _has_formatter_command = has_command(get_editor_setting(SETTING_FORMATTER_PATH)) - add_format_command() - add_lint_command() - add_install_update_command() - add_uninstall_command() - add_report_issue_command() - - menu = FormatterMenu.new() - add_child(menu) - menu.menu_item_selected.connect(_on_menu_item_selected) - menu.update_menu(is_formatter_installed_locally()) - - update_shortcut() - resource_saved.connect(_on_resource_saved) - - -func _exit_tree() -> void: - resource_saved.disconnect(_on_resource_saved) - - remove_format_command() - remove_lint_command() - remove_install_update_command() - remove_uninstall_command() - remove_report_issue_command() - - installer.queue_free() - installer = null - - if is_instance_valid(menu): - menu.menu_item_selected.disconnect(_on_menu_item_selected) - menu.remove_formatter_menu() - menu.queue_free() - menu = null - - -func _shortcut_input(event: InputEvent) -> void: - var shortcut := get_editor_setting(SETTING_SHORTCUT) as Shortcut - if not is_instance_valid(shortcut): - return - if not shortcut.matches_event(event) or not event.is_pressed() or event.is_echo(): - return - if format_current_script(): - get_tree().root.set_input_as_handled() - - -func format_current_script() -> bool: - if not is_formatter_available(): - return false - if not EditorInterface.get_script_editor().is_visible_in_tree(): - return false - var current_script := EditorInterface.get_script_editor().get_current_script() - if not is_instance_valid(current_script) or not current_script is GDScript: - return false - var code_edit: CodeEdit = EditorInterface \ - .get_script_editor() \ - .get_current_editor() \ - .get_base_editor() - - var formatted_code := format_code(current_script, false, code_edit.text) - if formatted_code.is_empty(): - return false - - reload_code_edit(code_edit, formatted_code) - return true - - -func lint_current_script() -> bool: - if not is_formatter_available(): - return false - if not EditorInterface.get_script_editor().is_visible_in_tree(): - return false - - var current_script := EditorInterface.get_script_editor().get_current_script() - if not is_instance_valid(current_script) or not current_script is GDScript: - return false - - var code_edit: CodeEdit = EditorInterface \ - .get_script_editor() \ - .get_current_editor() \ - .get_base_editor() - - var lint_issues := lint_code(current_script) - if lint_issues.is_empty(): - print("No linting issues found.") - clear_lint_highlights(code_edit) - return true - - apply_lint_highlights(code_edit, lint_issues) - print_lint_summary(lint_issues, current_script.resource_path) - - return true - - -func update_shortcut() -> void: - for obj: Resource in connection_list: - obj.changed.disconnect(update_shortcut) - - connection_list.clear() - - var shortcut := get_editor_setting(SETTING_SHORTCUT) as Shortcut - if is_instance_valid(shortcut): - for event: InputEvent in shortcut.events: - if is_instance_valid(event): - event.changed.connect(update_shortcut) - connection_list.push_back(event) - - remove_format_command() - add_format_command() - - -func _on_resource_saved(saved_resource: Resource) -> void: - if saved_resource is not GDScript: - return - var script := saved_resource as GDScript - var do_format_on_save := get_editor_setting(SETTING_FORMAT_ON_SAVE) as bool - - # We should normally never hit this condition, I'm adding it as a - # safety check and to document issues with format on save and built-in - # scripts. We support formatting built-in scripts, but not formatting them - # on save: - # - There's no way to retrieve built-in scripts and when their enclosing - # scene is saved efficiently - # - You would have to re-save the scene after formatting - # - # That would add complexity and slowdowns so we don't support it at the - # moment. We may revisit this if multiple users depend on this feature (I - # generally don't recommend built-in scripts because of their limitations - # for debugging, error reporting, VCS... they have too many limitations for - # their benefit) - if script.is_built_in(): - if do_format_on_save and not _already_warned_about_builtin_format_on_save: - push_warning( - "GDScript Formatter: Format on save is not supported for built-in scripts. " - + "Format this script manually instead." - ) - _already_warned_about_builtin_format_on_save = true - return - - var editorconfig_format_on_save = get_editorconfig_format_on_save(script.resource_path) - if editorconfig_format_on_save != null: - do_format_on_save = editorconfig_format_on_save as bool - var lint_on_save := get_editor_setting(SETTING_LINT_ON_SAVE) as bool - - if ( - do_format_on_save and get_format_mode() == FormatMode.REORDER_CODE - and not _already_warned_about_reorder_on_save - ): - push_warning( - "GDScript Formatter: Reorder code is enabled for format on save. It is usually better used manually." - ) - _already_warned_about_reorder_on_save = true - - if not do_format_on_save and not lint_on_save: - return - - var ignored_directories_normalized: Array[String] = [] - var ignored_directories_seen: Dictionary[String, String] = { } - for directory: String in get_editor_setting(SETTING_IGNORED_DIRECTORIES): - # We split paths on "/", we trim trailing slashes to avoid empty path - # segments that would never match when checking ignored directories. - var directory_normalized := directory.trim_prefix("res://").trim_suffix("/") - if directory_normalized.is_empty(): - push_warning( - "GDScript Formatter: Format on Save Ignored Directories entry \"%s\" " % directory - + "has no path after removing \"res://\" and trailing slashes, and will be skipped. " - + "This may mean you're trying to ignore the entire project, which isn't supported here. " - + "Please remove it from the list, enter a valid path, or turn off format on save instead." - ) - continue - - if ignored_directories_seen.has(directory_normalized): - push_warning( - "GDScript Formatter: Format on Save Ignored Directories entry \"%s\" " % directory - + "refers to the same directory as entry \"%s\" " - % ignored_directories_seen[directory_normalized] - + "and will be skipped. Please remove the duplicate entry from the list." - ) - continue - - ignored_directories_seen[directory_normalized] = directory - ignored_directories_normalized.push_back(directory_normalized) - - var saved_script_path_segments := script.resource_path.trim_prefix("res://").split("/") - for ignored_directory_path: String in ignored_directories_normalized: - var ignored_directory_segments := ignored_directory_path.split("/") - if ignored_directory_segments.size() > saved_script_path_segments.size(): - continue - - var matches := true - for i in range(ignored_directory_segments.size()): - if ignored_directory_segments[i] != saved_script_path_segments[i]: - matches = false - break - - if matches: - return - - if not is_formatter_available() or not is_instance_valid(script): - return - - if do_format_on_save: - var formatted_code := format_code(script, false) - if formatted_code.is_empty(): - return - - script.source_code = formatted_code - ResourceSaver.save(script) - # The argument (keep_state parameter) tells Godot to try to preserve the - # state of the script instance, like static variables. Without this, - # attempting to reload tool scripts will fail with an error because they - # are already instantiated in the editor and instantiated scripts are - # not allowed to force reload without unloading first. - script.reload(true) - - var script_editor := EditorInterface.get_script_editor() - var open_script_editors := script_editor.get_open_script_editors() - var open_scripts := script_editor.get_open_scripts() - - if not open_scripts.has(script): - return - - if script_editor.get_current_script() == script: - reload_code_edit( - script_editor.get_current_editor().get_base_editor(), - formatted_code, - true, - ) - elif open_scripts.size() == open_script_editors.size(): - for i: int in range(open_scripts.size()): - if open_scripts[i] == script: - reload_code_edit(open_script_editors[i].get_base_editor(), formatted_code, true) - return - else: - push_error( - "GDScript Formatter error: Unknown situation, can't reload code editor in Editor. Please report this issue." - ) - - if lint_on_save: - var code_edit: CodeEdit = EditorInterface \ - .get_script_editor() \ - .get_current_editor() \ - .get_base_editor() - var lint_issues := lint_code(script) - if lint_issues.is_empty(): - clear_lint_highlights(code_edit) - else: - apply_lint_highlights(code_edit, lint_issues) - print_lint_summary(lint_issues, script.resource_path) - - -func add_format_command() -> void: - if _has_format_command: - return - var formatter_path := get_editor_setting(SETTING_FORMATTER_PATH) as String - if formatter_path.is_empty() or not _has_formatter_command: - if not formatter_path.is_empty(): - push_error( - 'GDScript Formatter: The command "%s" can\'t be found in your environment.\n' - % formatter_path - + '\tIf you have not installed the formatter, use the install/update command from the command palette.\n' - + '\tIf you have installed the formatter, change "formatter_path" to a valid command in the "GDScript Formatter" section in Editor Settings.', - ) - return - var shortcut := get_editor_setting(SETTING_SHORTCUT) as Shortcut - EditorInterface.get_command_palette().add_command( - COMMAND_PALETTE_FORMAT_SCRIPT, - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_FORMAT_SCRIPT, - format_current_script, - shortcut.get_as_text() if is_instance_valid(shortcut) else "None", - ) - _has_format_command = true - - -func remove_format_command() -> void: - if not _has_format_command: - return - EditorInterface.get_command_palette().remove_command( - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_FORMAT_SCRIPT - ) - _has_format_command = false - - -func add_lint_command() -> void: - if not _has_formatter_command or _has_lint_command: - return - - EditorInterface.get_command_palette().add_command( - COMMAND_PALETTE_LINT_SCRIPT, - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_LINT_SCRIPT, - lint_current_script, - ) - _has_lint_command = true - - -func remove_lint_command() -> void: - if not _has_lint_command: - return - EditorInterface.get_command_palette().remove_command( - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_LINT_SCRIPT, - ) - _has_lint_command = false - - -func add_install_update_command() -> void: - EditorInterface.get_command_palette().add_command( - COMMAND_PALETTE_INSTALL_UPDATE, - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_INSTALL_UPDATE, - installer.install_or_update_formatter, - ) - - -func remove_install_update_command() -> void: - EditorInterface.get_command_palette().remove_command( - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_INSTALL_UPDATE - ) - - -func add_uninstall_command() -> void: - if is_formatter_installed_locally(): - EditorInterface.get_command_palette().add_command( - COMMAND_PALETTE_UNINSTALL, - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_UNINSTALL, - uninstall_formatter, - ) - _has_uninstall_command = true - - -func remove_uninstall_command() -> void: - if not _has_uninstall_command: - return - EditorInterface.get_command_palette().remove_command( - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_UNINSTALL - ) - _has_uninstall_command = false - - -func add_report_issue_command() -> void: - EditorInterface.get_command_palette().add_command( - COMMAND_PALETTE_REPORT_ISSUE, - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_REPORT_ISSUE, - report_issue, - ) - - -func remove_report_issue_command() -> void: - EditorInterface.get_command_palette().remove_command( - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_REPORT_ISSUE - ) - - -func has_command(command: String) -> bool: - if command.is_empty(): - return false - var output: Array = [] - var exit_code := OS.execute(command, ["--version"], output, true) - return exit_code == OK - - -func is_formatter_available() -> bool: - if _has_formatter_command: - return true - _has_formatter_command = has_command(get_editor_setting(SETTING_FORMATTER_PATH)) - return _has_formatter_command - - -func is_formatter_installed_locally() -> bool: - var binary_name := "gdscript-formatter" - if OS.get_name().to_lower().contains("windows"): - binary_name = "gdscript-formatter.exe" - var binary_path := formatter_cache_dir.path_join(binary_name) - return FileAccess.file_exists(binary_path) - - -func uninstall_formatter() -> void: - var binary_name := "gdscript-formatter" - if OS.get_name().to_lower().contains("windows"): - binary_name = "gdscript-formatter.exe" - var binary_path := formatter_cache_dir.path_join(binary_name) - - if FileAccess.file_exists(binary_path): - DirAccess.remove_absolute(binary_path) - print("GDScript formatter uninstalled successfully from: ", binary_path) - set_editor_setting(SETTING_FORMATTER_PATH, DEFAULT_SETTINGS[SETTING_FORMATTER_PATH]) - _has_formatter_command = false - - remove_format_command() - remove_lint_command() - add_format_command() - remove_uninstall_command() - add_uninstall_command() - if is_instance_valid(menu): - menu.update_menu(false) - else: - push_error("GDScript formatter not found in cache directory: ", binary_path) - - -func reorder_code() -> bool: - if not is_formatter_available(): - return false - if not EditorInterface.get_script_editor().is_visible_in_tree(): - return false - var current_script := EditorInterface.get_script_editor().get_current_script() - if not is_instance_valid(current_script) or not current_script is GDScript: - return false - var code_edit: CodeEdit = EditorInterface \ - .get_script_editor() \ - .get_current_editor() \ - .get_base_editor() - - var formatted_code := format_code(current_script, true, code_edit.text) - if formatted_code.is_empty(): - return false - - reload_code_edit(code_edit, formatted_code) - return true - - -func report_issue() -> void: - OS.shell_open("https://github.com/GDQuest/GDScript-formatter/issues") - - -func show_help() -> void: - OS.shell_open("https://www.gdquest.com/library/gdscript_formatter/") - - -func _on_menu_item_selected(command: String) -> void: - match command: - "format_script": - format_current_script() - "lint_script": - lint_current_script() - "reorder_code": - reorder_code() - "install_update": - installer.install_or_update_formatter() - "uninstall": - uninstall_formatter() - "report_issue": - report_issue() - "help": - show_help() - _: - push_warning("Unsupported command sent from the menu: " + command) - - -## Reloads the code editor with new text while preserving editor state. -## This includes cursor position, scroll position, breakpoints, bookmarks, and folds. -func reload_code_edit(code_edit: CodeEdit, new_text: String, tag_saved := false) -> void: - var editor_state := CodeEditState.new(code_edit) - code_edit.text = new_text - if tag_saved: - code_edit.tag_saved_version() - editor_state.restore_to_editor(code_edit) - code_edit.update_minimum_size() - code_edit.text_changed.emit() - - -func get_editor_setting(setting_name: String) -> Variant: - var editor_settings := EditorInterface.get_editor_settings() - var full_setting_key := EDITOR_SETTINGS_CATEGORY + setting_name - if editor_settings.has_setting(full_setting_key): - return editor_settings.get_setting(full_setting_key) - return DEFAULT_SETTINGS[setting_name] - - -func set_editor_setting(setting_name: String, value: Variant) -> void: - var editor_settings := EditorInterface.get_editor_settings() - var full_setting_key := EDITOR_SETTINGS_CATEGORY + setting_name - editor_settings.set_setting(full_setting_key, value) - - -func has_editor_setting(setting_name: String) -> bool: - var editor_settings := EditorInterface.get_editor_settings() - var full_setting_key := EDITOR_SETTINGS_CATEGORY + setting_name - return editor_settings.has_setting(full_setting_key) - - -## Returns true if this script should be formatted automatically on save, based -## on the project's .editorconfig file. Returns false if the config says not to -## format on save. Returns null if no rule matches (then it's the user editor -## settings that take over). -func get_editorconfig_format_on_save(script_path: String) -> Variant: - var editorconfig_path := ProjectSettings.globalize_path("res://.editorconfig") - var modified_time := FileAccess.get_modified_time(editorconfig_path) - if modified_time != _editorconfig_last_modified_time: - _editorconfig_last_modified_time = modified_time - _editorconfig_format_on_save_rules.clear() - load_editorconfig_format_on_save_rules(editorconfig_path) - - var relative_script_path := script_path.trim_prefix("res://") - var format_on_save = null - for rule: Dictionary in _editorconfig_format_on_save_rules: - var pattern := rule["pattern"] as String - if pattern.is_empty() or editorconfig_section_matches(pattern, relative_script_path): - format_on_save = rule["format_on_save"] - - return format_on_save - - -## Loads the project editorconfig file and parses format on save rules. -func load_editorconfig_format_on_save_rules(editorconfig_path: String) -> void: - var editorconfig_file := FileAccess.open(editorconfig_path, FileAccess.READ) - if editorconfig_file == null: - return - - var pattern := "" - - while not editorconfig_file.eof_reached(): - var line := editorconfig_file.get_line().strip_edges() - if line.is_empty() or line.begins_with("#") or line.begins_with(";"): - continue - - if line.begins_with("[") and line.ends_with("]"): - pattern = line.trim_prefix("[").trim_suffix("]") - continue - - if not line.contains("="): - continue - - var key_and_value := line.split("=", true, 1) - if key_and_value[0].strip_edges().to_lower() != "gdscript_formatter_format_on_save": - continue - - match key_and_value[1].strip_edges().to_lower(): - "true": - _editorconfig_format_on_save_rules.append( - { "pattern": pattern, "format_on_save": true } - ) - "false": - _editorconfig_format_on_save_rules.append( - { "pattern": pattern, "format_on_save": false } - ) - - editorconfig_file.close() - - -## Returns true if an EditorConfig section applies to a saved script. -## pattern: The EditorConfig pattern to match against. -## relative_script_path: The path of the script relative to the project root. -func editorconfig_section_matches(pattern: String, relative_script_path: String) -> bool: - if pattern.is_empty(): - return false - - var normalized_pattern := pattern.trim_prefix("/") - var matching_path := relative_script_path - # If there's no / in the pattern it means this pattern targets a filename. - # It's not a folder/path glob pattern. - if not normalized_pattern.contains("/"): - matching_path = relative_script_path.get_file() - - return matching_path.match(normalized_pattern) - - -## Formats GDScript code using the GDScript Formatter and returns it as a string. -## When source_content is null, reads the code from the GDScript resource directly. -## Otherwise, formats source_content without reading from the file. -## -## Pass a string through source_content when the user is editing the script in -## the editor and requests formatting without having saved their changes (in -## that case, the code they're editing only exists in the script editor's open -## tab). -func format_code( - script: GDScript, - force_reorder := false, - source_content: Variant = null, -) -> String: - var script_path := script.resource_path - if source_content == null and script_path.is_empty(): - push_error("GDScript Formatter Error: Can't format an unsaved script.") - return "" - - # Source content is not set, read from the GDScript resource instead. - # - # This is a bit of a hack to avoid two issues: - # - # 1. Running GDScript formatter on stdin/stdout through Godot with - # OS.execute() has encoding issues with UTF-8 characters and we don't have - # control over the output encoding (it might be assuming ASCII characters) - # - # 2. If we modify a file in place using the external formatter from Godot, - # it will bring up a pop-up that warns users that the file has been changed - # outside Godot. - # - # To work around that, I save a copy of the script as a temporary file, - # format the file, and read it specifically as a UTF-8 string. - if source_content == null: - var source_file := FileAccess.open( - ProjectSettings.globalize_path(script_path), - FileAccess.READ, - ) - if not source_file: - push_error("GDScript Formatter Error: Cannot read source file: " + script_path) - return "" - - # FileAccess.get_as_text() reads the file as UTF-8. We use it here and after - # formatting the temporary file. - source_content = source_file.get_as_text() - source_file.close() - - var path_temporary_file := OS.get_temp_dir().path_join( - "gdscript_formatter_%d.gd" % Time.get_ticks_msec() - ) - var temporary_file := FileAccess.open(path_temporary_file, FileAccess.WRITE) - if temporary_file == null: - push_error("GDScript Formatter Error: Cannot create temporary file: " + path_temporary_file) - return "" - temporary_file.store_string(source_content as String) - temporary_file.close() - - var formatter_arguments := PackedStringArray() - if get_editor_setting(SETTING_USE_SPACES): - formatter_arguments.push_back("--use-spaces") - formatter_arguments.push_back("--indent-size=%d" % get_editor_setting(SETTING_INDENT_SIZE)) - - var format_mode := get_format_mode() - var should_reorder := force_reorder or format_mode == FormatMode.REORDER_CODE - - if should_reorder: - formatter_arguments.push_back("--reorder-code") - - if not force_reorder and format_mode == FormatMode.VERIFY_STRUCTURE: - # NB: this is a deprecated flag, replaced with --verify-structure, but - # we keep it here for users that have a previous version of the - # formatter installed. - formatter_arguments.push_back("--safe") - - formatter_arguments.push_back(path_temporary_file) - - var output: Array = [] - var exit_code := OS.execute( - get_editor_setting(SETTING_FORMATTER_PATH), - formatter_arguments, - output, - true, - ) - - var formatted_content := "" - if exit_code == OK: - var result_file := FileAccess.open(path_temporary_file, FileAccess.READ) - if result_file: - formatted_content = result_file.get_as_text() - result_file.close() - else: - push_error("Format GDScript: Cannot read formatted output from temp file") - else: - push_error( - "Format GDScript failed: " - + (script_path if not script_path.is_empty() else "unsaved script") - ) - push_error( - "\tExit code: " + str(exit_code) + " Output: " - + (output[0].strip_edges() if output.size() > 0 else "No output"), - ) - push_error( - '\tIf your script does not have any syntax errors, this might be a formatter bug.' - ) - - if FileAccess.file_exists(path_temporary_file): - DirAccess.remove_absolute(path_temporary_file) - - return formatted_content - - -func get_format_mode() -> int: - return get_editor_setting(SETTING_FORMAT_MODE) as int - - -## Lints a GDScript file using the GDScript Formatter's linter, -## and returns an array of lint issues. -func lint_code(script: GDScript) -> Array: - var script_path := script.resource_path - var output: Array = [] - var formatter_arguments: Array = ["lint", ProjectSettings.globalize_path(script_path)] - - var max_line_length := get_editor_setting(SETTING_LINT_LINE_LENGTH) as int - formatter_arguments.append("--max-line-length") - formatter_arguments.append(str(max_line_length)) - - var ignored_rules := get_editor_setting(SETTING_LINT_IGNORED_RULES) as String - if not ignored_rules.is_empty(): - formatter_arguments.append("--disable") - formatter_arguments.append(ignored_rules) - - var exit_code := OS.execute( - get_editor_setting(SETTING_FORMATTER_PATH), - formatter_arguments, - output, - ) - if exit_code == OK: - return [] # No issues found - - if exit_code == 1: - # Parse lint output - the output is a single string with multiple lines - var issues = [] - for output_item in output: - var lines = output_item.split("\n") - for line in lines: - var trimmed_line = line.strip_edges() - if trimmed_line.is_empty(): - continue - var issue = parse_lint_issue(trimmed_line) - if issue != null and not issue.is_empty(): - issues.push_back(issue) - return issues - - push_error("Lint GDScript failed: " + script_path) - push_error( - "\tExit code: " + str(exit_code) + " Output: " - + (output.front().strip_edges() if output.size() > 0 else "No output") - ) - return [] - - -## Parses a lint issue line and returns a dictionary with issue information -func parse_lint_issue(line: String) -> Dictionary: - # Expected format: filename:line:rule:severity: message - var regex = RegEx.new() - regex.compile(r"^(.*\.gd):(\d+):([^:]+):([^:]+):([\s\S]*)$") - var result = regex.search(line) - if result: - return { - "line": int(result.get_string(2)) - 1, - "rule": result.get_string(3), - "severity": result.get_string(4), - "message": result.get_string(5).strip_edges(), - } - return { } - - -## Applies lint highlighting to the code editor -func apply_lint_highlights(code_edit: CodeEdit, issues: Array) -> void: - clear_lint_highlights(code_edit) - - # Add and set up gutter for lint icons if not already present. - # We check by name to avoid conflicts with gutters added by other addons. - # Once added, the gutter is never removed so the layout doesn't shift on clear. - var has_lint_gutter := false - for i: int in code_edit.get_gutter_count(): - if code_edit.get_gutter_name(i) == GUTTER_LINT_ICONS_NAME: - has_lint_gutter = true - break - if not has_lint_gutter: - code_edit.add_gutter(GUTTER_LINT_ICON_INDEX) - code_edit.set_gutter_name(GUTTER_LINT_ICON_INDEX, GUTTER_LINT_ICONS_NAME) - code_edit.set_gutter_type(GUTTER_LINT_ICON_INDEX, CodeEdit.GutterType.GUTTER_TYPE_ICON) - const EDITOR_ICON_DEFAULT_WIDTH = 16.0 - code_edit.set_gutter_width( - GUTTER_LINT_ICON_INDEX, - EDITOR_ICON_DEFAULT_WIDTH * EditorInterface.get_editor_scale(), - ) - - for issue in issues: - var line_number: int = issue.line - var severity: String = issue.severity - - var color := Color(1, 0, 0, 0.1) if severity == "error" else Color(1, 1, 0, 0.1) - code_edit.set_line_background_color(line_number, color) - var icon_name := "StatusError" if severity == "error" else "StatusWarning" - var icon := EditorInterface.get_editor_theme().get_icon(icon_name, "EditorIcons") - code_edit.set_line_gutter_icon(line_number, GUTTER_LINT_ICON_INDEX, icon) - - -## Prints a detailed summary of lint issues to the output -func print_lint_summary(issues: Array, script_path: String) -> void: - print_rich("\n[b]=== Linting Results for %s ===[/b]\n" % script_path) - print_rich("[b]Found [i]%s[/i] issue(s)\n[/b]" % issues.size()) - - for issue in issues: - var line_display = str(issue.line + 1) # Convert back to 1-based for display - var severity_label = issue.severity.to_upper() - print_rich( - "[color=%s]%s[/color] on line [color=cyan]%s[/color] ([i]%s[/i])" - % [ - "red" if severity_label == "ERROR" else "yellow", - severity_label, - line_display, - issue.rule, - ], - ) - print_rich("[i]%s[/i]\n" % [issue.message]) - - print_rich("[b]=== End Linting Results ===[/b]\n") - - -## Clears all lint highlighting from the code editor. -## The lint gutter is intentionally kept so the layout does not shift. -func clear_lint_highlights(code_edit: CodeEdit) -> void: - var lint_gutter_index := -1 - for i: int in code_edit.get_gutter_count(): - if code_edit.get_gutter_name(i) == GUTTER_LINT_ICONS_NAME: - lint_gutter_index = i - break - - for line in range(code_edit.get_line_count()): - code_edit.set_line_background_color(line, Color(0, 0, 0, 0)) - if lint_gutter_index != -1: - code_edit.set_line_gutter_icon(line, lint_gutter_index, null) - - -## Data structure to hold code editor state information -class CodeEditState: - var caret_line: int - var caret_column: int - var horizontal_scroll: int - var vertical_scroll: int - var breakpoints: Dictionary[int, String] = { } - var bookmarks: Dictionary[int, String] = { } - var folds: Dictionary[int, String] = { } - var code_edit: CodeEdit - - - func _init(code_edit: CodeEdit) -> void: - self.code_edit = code_edit - caret_line = code_edit.get_caret_line() - caret_column = code_edit.get_caret_column() - horizontal_scroll = code_edit.scroll_horizontal - vertical_scroll = code_edit.scroll_vertical - - for line in code_edit.get_breakpointed_lines(): - breakpoints[line] = code_edit.get_line(line) - for line in code_edit.get_bookmarked_lines(): - bookmarks[line] = code_edit.get_line(line) - for line in code_edit.get_folded_lines(): - folds[line] = code_edit.get_line(line) - - - func restore_to_editor(code_edit: CodeEdit) -> void: - var new_line_count := code_edit.get_line_count() - - _restore_line_features(breakpoints, code_edit.set_line_as_breakpoint, new_line_count) - _restore_line_features(bookmarks, code_edit.set_line_as_bookmarked, new_line_count) - _restore_line_features( - folds, - func(line: int, _is_folded: bool) -> void: - code_edit.fold_line(line), - new_line_count, - ) - - code_edit.set_caret_line(caret_line) - code_edit.set_caret_column(caret_column) - - code_edit.scroll_horizontal = horizontal_scroll - code_edit.scroll_vertical = vertical_scroll - - - ## Restores line-based features (breakpoints, bookmarks, folds) by finding the best matching lines - ## in the new text based on similarity to the original line text. - ## - ## Big thanks to https://github.com/Daylily-Zeleen/GDScript-Formatter for this approach. - func _restore_line_features( - stored_features: Dictionary, - set_line_func: Callable, - new_line_count: int, - ) -> void: - var stored_lines := PackedInt64Array(stored_features.keys()) - for line_index in range(stored_lines.size()): - var original_line := stored_lines[line_index] as int - var original_text := stored_features[original_line] as String - - # After formatting lines can move, so we need to find the best match for the original line - # to restore the breakpoints, bookmarks, and folds. - # We first check the same line, then we expand our search outwards until we find a match. - # We use a similarity threshold of 0.9 to account for minor changes in the line text. - # This should be sufficient for most cases, but might need adjustment for edge cases. - # If no match is found, we skip restoring this feature - if ( - original_line < new_line_count - and code_edit.get_line(original_line).similarity(original_text) > 0.9 - ): - set_line_func.call(original_line, true) - continue - - var line_above := original_line - 1 - var line_below := original_line + 1 - while line_above >= 0 or line_below < new_line_count: - if line_below < new_line_count and code_edit.get_line(line_below).similarity( - original_text - ) > 0.9: - set_line_func.call(line_below, true) - break - if line_above >= 0 and code_edit.get_line(line_above).similarity(original_text) > 0.9: - set_line_func.call(line_above, true) - break - - line_above -= 1 - line_below += 1 diff --git a/addons/GDQuest_GDScript_formatter/plugin.gd.uid b/addons/GDQuest_GDScript_formatter/plugin.gd.uid deleted file mode 100644 index f49f0cf..0000000 --- a/addons/GDQuest_GDScript_formatter/plugin.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dqgr353xopp3y diff --git a/addons/GDQuest_GDScript_formatter_standalone/LICENSE b/addons/GDQuest_GDScript_formatter_standalone/LICENSE deleted file mode 100644 index d2053ea..0000000 --- a/addons/GDQuest_GDScript_formatter_standalone/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025-present GDQuest - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/addons/GDQuest_GDScript_formatter_standalone/bin/gdscript_formatter.gdextension b/addons/GDQuest_GDScript_formatter_standalone/bin/gdscript_formatter.gdextension deleted file mode 100644 index 343642f..0000000 --- a/addons/GDQuest_GDScript_formatter_standalone/bin/gdscript_formatter.gdextension +++ /dev/null @@ -1,6 +0,0 @@ -[configuration] -entry_symbol = "gdext_rust_init" -compatibility_minimum = 4.7 - -[libraries] -linux.debug.x86_64 = "res://addons/GDQuest_GDScript_formatter_standalone/bin/libgdscript_formatter_gdextension.so" diff --git a/addons/GDQuest_GDScript_formatter_standalone/bin/gdscript_formatter.gdextension.uid b/addons/GDQuest_GDScript_formatter_standalone/bin/gdscript_formatter.gdextension.uid deleted file mode 100644 index 8e8a190..0000000 --- a/addons/GDQuest_GDScript_formatter_standalone/bin/gdscript_formatter.gdextension.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dwlskvf853una diff --git a/addons/GDQuest_GDScript_formatter_standalone/menu.gd b/addons/GDQuest_GDScript_formatter_standalone/menu.gd deleted file mode 100644 index a0847dd..0000000 --- a/addons/GDQuest_GDScript_formatter_standalone/menu.gd +++ /dev/null @@ -1,145 +0,0 @@ -## This module handles adding a menu to the script editor with formatter commands. -## It safely locates the script editor's menu bar and adds our custom menu. -@tool -extends Node - -signal menu_item_selected(command: String) - -const MENU_TEXT = "Format" -const MENU_ITEMS = { - "format_script": "Format Current Script", - "lint_script": "Lint Current Script", - "reorder_code": "Reorder Code", - "update": "Update Formatter", - "report_issue": "Report Issue", - "help": "Help", -} - -var menu_button: MenuButton = null -var popup_menu: PopupMenu = null - - -func _ready() -> void: - # At the start we insert the menu in the script editor - var script_editor := EditorInterface.get_script_editor() - var last_menu_button := _find_last_menu_button(script_editor) - if not is_instance_valid(last_menu_button): - push_warning("GDScript Formatter: Could not find valid menu button in script editor. Menu will not be available. Use the command palette instead.") - return - - menu_button = MenuButton.new() - menu_button.text = MENU_TEXT - menu_button.switch_on_hover = true - menu_button.flat = false - menu_button.theme_type_variation = &"FlatMenuButton" - - popup_menu = menu_button.get_popup() - _populate_menu() - - popup_menu.id_pressed.connect(_on_menu_item_pressed) - last_menu_button.add_sibling(menu_button) - - -## Cleans up the menu from the script editor. Call this when disabling the -## plugin (in _exit_tree()). -func remove_formatter_menu() -> void: - if is_instance_valid(menu_button): - if is_instance_valid(popup_menu): - popup_menu.id_pressed.disconnect(_on_menu_item_pressed) - menu_button.queue_free() - menu_button = null - popup_menu = null - - -func update_menu() -> void: - if not is_instance_valid(popup_menu): - return - popup_menu.clear() - _populate_menu() - - -## Searches for and returns the last menu node in the script editor top menu bar, -## or null if not found. -func _find_last_menu_button(script_editor: Control) -> MenuButton: - # The first child of the script editor should be a VBoxContainer (main container for the script editor main screen) - # Then the first child of that container should be an HBoxContainer (the menu bar) - # This is based on the current structure of Godot's script editor as of Godot 4.5 - # Note: We add multiple checks in there with null returns mainly in case something changes in a future - if script_editor.get_child_count() == 0: - return null - - var main_container := script_editor.get_child(0) - if not is_instance_valid(main_container) or not main_container is VBoxContainer: - return null - - if main_container.get_child_count() == 0: - return null - - var menu_bar := main_container.get_child(0) - if not is_instance_valid(menu_bar) or not menu_bar is HBoxContainer: - return null - - # We reached the menu bar. So now we loop through all the children look for - # the last menu button, which would be the last menu in the menu list. - # Note: Here the goal is to insert the menu after the debug menu, but we - # don't check for the button text because "debug" would only work in English - # this should work in any language. - var last_menu_button: MenuButton = null - for child in menu_bar.get_children(): - if child is MenuButton: - last_menu_button = child as MenuButton - - return last_menu_button - - -func _populate_menu() -> void: - if not is_instance_valid(popup_menu): - return - - var current_item_index := 0 - - popup_menu.add_item(MENU_ITEMS["format_script"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "format_script") - popup_menu.set_item_tooltip(current_item_index, "Run the GDScript Formatter over the current script") - - current_item_index += 1 - popup_menu.add_item(MENU_ITEMS["lint_script"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "lint_script") - popup_menu.set_item_tooltip(current_item_index, "Check the current script for linting issues") - - current_item_index += 1 - popup_menu.add_item(MENU_ITEMS["reorder_code"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "reorder_code") - popup_menu.set_item_tooltip(current_item_index, "Reorder the code elements in the current script according to the GDScript Style Guide") - - popup_menu.add_separator() - - # NOTE: When we add separators, it bumps the internal index of menu items. - # That's why we have to increase it even on separators. Otherwise the - # tooltip will lose sync. - current_item_index += 2 - popup_menu.add_item(MENU_ITEMS["update"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "update") - popup_menu.set_item_tooltip(current_item_index, "Download the latest version of the GDScript Formatter") - - popup_menu.add_separator() - - # Bumped index by 1 extra step because of the previous separator. - current_item_index += 2 - popup_menu.add_item(MENU_ITEMS["report_issue"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "report_issue") - popup_menu.set_item_tooltip(current_item_index, "Tell us about problems or bugs you found") - - current_item_index += 1 - popup_menu.add_item(MENU_ITEMS["help"], current_item_index) - popup_menu.set_item_metadata(current_item_index, "help") - popup_menu.set_item_tooltip(current_item_index, "Learn how to use the GDScript Formatter") - - -func _on_menu_item_pressed(id: int) -> void: - if not is_instance_valid(popup_menu): - return - - var command: String = popup_menu.get_item_metadata(id) - if not command.is_empty(): - menu_item_selected.emit(command) diff --git a/addons/GDQuest_GDScript_formatter_standalone/menu.gd.uid b/addons/GDQuest_GDScript_formatter_standalone/menu.gd.uid deleted file mode 100644 index f533ab6..0000000 --- a/addons/GDQuest_GDScript_formatter_standalone/menu.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bqwpllr5ylrmb diff --git a/addons/GDQuest_GDScript_formatter_standalone/plugin.cfg b/addons/GDQuest_GDScript_formatter_standalone/plugin.cfg deleted file mode 100644 index 0fa1262..0000000 --- a/addons/GDQuest_GDScript_formatter_standalone/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="GDQuest GDScript Formatter (Standalone)" -description="A plugin to format GDScript code in the Godot editor using the GDScript Formatter from GDQuest." -author="GDQuest" -version="0.22.2" -script="plugin.gd" diff --git a/addons/GDQuest_GDScript_formatter_standalone/plugin.gd b/addons/GDQuest_GDScript_formatter_standalone/plugin.gd deleted file mode 100644 index 726805e..0000000 --- a/addons/GDQuest_GDScript_formatter_standalone/plugin.gd +++ /dev/null @@ -1,641 +0,0 @@ -## This plug-in adds support for automatically formatting GDScript files on save -## and via a command in the Godot Editor, using the GDQuest GDScript Formatter. -## -## It also provides an option to install or update the formatter binary from the GitHub releases. -## -## See our website for more information on the formatter and how to use it: -## https://www.gdquest.com/library/gdscript_formatter/ -@tool -extends EditorPlugin - -const FormatterMenu = preload("menu.gd") - -const EDITOR_SETTINGS_CATEGORY = "gdquest_gdscript_formatter/" -const SETTING_FORMAT_ON_SAVE = "format_on_save" -const SETTING_SHORTCUT = "shortcut" -const SETTING_USE_SPACES = "use_spaces" -const SETTING_INDENT_SIZE = "indent_size" -const SETTING_REORDER_CODE = "reorder_code" -const SETTING_SAFE_MODE = "safe_mode" -const SETTING_LINT_ON_SAVE = "lint_on_save" -const SETTING_LINT_LINE_LENGTH = "lint_line_length" -const SETTING_LINT_IGNORED_RULES = "lint_ignored_rules" -# Directories to ignore when Format on Save is enabled -const SETTING_IGNORED_DIRECTORIES = "format_on_save_ignored_directories" - -const COMMAND_PALETTE_CATEGORY = "gdquest gdscript formatter/" -const COMMAND_PALETTE_FORMAT_SCRIPT = "Format GDScript" -const COMMAND_PALETTE_LINT_SCRIPT = "Lint GDScript" -const COMMAND_PALETTE_UPDATE = "Update Formatter" -const COMMAND_PALETTE_REPORT_ISSUE = "Report Issue" - -var DEFAULT_SETTINGS = { - SETTING_FORMAT_ON_SAVE: false, - SETTING_USE_SPACES: false, - SETTING_INDENT_SIZE: 4, - SETTING_REORDER_CODE: false, - SETTING_SAFE_MODE: true, - SETTING_LINT_ON_SAVE: false, - SETTING_LINT_LINE_LENGTH: 100, - SETTING_LINT_IGNORED_RULES: "", - SETTING_IGNORED_DIRECTORIES: PackedStringArray(["addons/"]), -} - -## Which gutter lint icons are shown in. -## By default, gutter 0 is for breakpoints and 1 is for things like overrides. -const GUTTER_LINT_ICON_INDEX = 2 -const GUTTER_LINT_ICONS_NAME = "gdscript_formatter_lint_icons" - -var connection_list: Array[Resource] = [] -var menu: FormatterMenu = null -# Used to auto detect changes to the project's .editorconfig file. -var _editorconfig_last_modified_time := -1 -# Editorconfig allows setting rules per path glob. We track globs for the format -# on save rule here so users can enable it selectively for specific folders. -var _editorconfig_format_on_save_rules: Array[Dictionary] = [] - - -func _init() -> void: - for setting: String in DEFAULT_SETTINGS.keys(): - if not has_editor_setting(setting): - set_editor_setting(setting, DEFAULT_SETTINGS[setting]) - - if not has_editor_setting(SETTING_SHORTCUT): - var default_shortcut := InputEventKey.new() - default_shortcut.echo = false - default_shortcut.pressed = true - default_shortcut.keycode = KEY_I - default_shortcut.ctrl_pressed = true - default_shortcut.shift_pressed = false - default_shortcut.alt_pressed = true - - var shortcut := Shortcut.new() - shortcut.events.push_back(default_shortcut) - - set_editor_setting(SETTING_SHORTCUT, shortcut) - - -func _enter_tree() -> void: - add_format_command() - add_lint_command() - add_update_command() - add_report_issue_command() - - menu = FormatterMenu.new() - add_child(menu) - menu.menu_item_selected.connect(_on_menu_item_selected) - menu.update_menu() - - update_shortcut() - resource_saved.connect(_on_resource_saved) - - -func _exit_tree() -> void: - resource_saved.disconnect(_on_resource_saved) - - remove_format_command() - remove_lint_command() - remove_update_command() - remove_report_issue_command() - - if is_instance_valid(menu): - menu.menu_item_selected.disconnect(_on_menu_item_selected) - menu.remove_formatter_menu() - menu.queue_free() - menu = null - - -func _shortcut_input(event: InputEvent) -> void: - var shortcut := get_editor_setting(SETTING_SHORTCUT) as Shortcut - if not is_instance_valid(shortcut): - return - if not shortcut.matches_event(event) or not event.is_pressed() or event.is_echo(): - return - if format_current_script(): - get_tree().root.set_input_as_handled() - - -func format_current_script() -> bool: - if not EditorInterface.get_script_editor().is_visible_in_tree(): - return false - var current_script := EditorInterface.get_script_editor().get_current_script() - if not is_instance_valid(current_script) or not current_script is GDScript: - return false - var code_edit: CodeEdit = EditorInterface.get_script_editor().get_current_editor().get_base_editor() - - var formatted_code := format_code(current_script, false, code_edit.text) - if formatted_code.is_empty(): - return false - - reload_code_edit(code_edit, formatted_code) - return true - - -func lint_current_script() -> bool: - if not EditorInterface.get_script_editor().is_visible_in_tree(): - return false - - var current_script := EditorInterface.get_script_editor().get_current_script() - if not is_instance_valid(current_script) or not current_script is GDScript: - return false - - var code_edit: CodeEdit = EditorInterface.get_script_editor().get_current_editor().get_base_editor() - - var lint_issues := lint_code(current_script) - if lint_issues.is_empty(): - print("No linting issues found.") - clear_lint_highlights(code_edit) - return true - - apply_lint_highlights(code_edit, lint_issues) - print_lint_summary(lint_issues, current_script.resource_path) - - return true - - -func update_shortcut() -> void: - for obj: Resource in connection_list: - obj.changed.disconnect(update_shortcut) - - connection_list.clear() - - var shortcut := get_editor_setting(SETTING_SHORTCUT) as Shortcut - if is_instance_valid(shortcut): - for event: InputEvent in shortcut.events: - if is_instance_valid(event): - event.changed.connect(update_shortcut) - connection_list.push_back(event) - - remove_format_command() - add_format_command() - - -func _on_resource_saved(saved_resource: Resource) -> void: - if saved_resource is not GDScript: - return - - var script := saved_resource as GDScript - var do_format_on_save := get_editor_setting(SETTING_FORMAT_ON_SAVE) as bool - var editorconfig_format_on_save = get_editorconfig_format_on_save(script.resource_path) - if editorconfig_format_on_save != null: - do_format_on_save = editorconfig_format_on_save as bool - var lint_on_save := get_editor_setting(SETTING_LINT_ON_SAVE) as bool - - if not do_format_on_save and not lint_on_save: - return - - var ignored_directories = get_editor_setting(SETTING_IGNORED_DIRECTORIES) - var path = script.resource_path.trim_prefix("res://") - - var script_path_parts := path.split("/") - - for directory: String in ignored_directories: - var normalized_dir := directory.trim_prefix("res://") - var directory_parts := normalized_dir.split("/") - - var matches := true - for i in range(directory_parts.size()): - if directory_parts[i] != script_path_parts[i]: - matches = false - break - - if matches: - return - - if not is_instance_valid(script): - return - - if do_format_on_save: - var formatted_code := format_code(script, false) - if formatted_code.is_empty(): - return - - script.source_code = formatted_code - ResourceSaver.save(script) - # The argument (keep_state parameter) tells Godot to try to preserve the - # state of the script instance, like static variables. Without this, - # attempting to reload tool scripts will fail with an error because they - # are already instantiated in the editor and instantiated scripts are - # not allowed to force reload without unloading first. - script.reload(true) - - var script_editor := EditorInterface.get_script_editor() - var open_script_editors := script_editor.get_open_script_editors() - var open_scripts := script_editor.get_open_scripts() - - if not open_scripts.has(script): - return - - if script_editor.get_current_script() == script: - reload_code_edit(script_editor.get_current_editor().get_base_editor(), formatted_code, true) - elif open_scripts.size() == open_script_editors.size(): - for i: int in range(open_scripts.size()): - if open_scripts[i] == script: - reload_code_edit(open_script_editors[i].get_base_editor(), formatted_code, true) - return - else: - push_error("GDScript Formatter error: Unknown situation, can't reload code editor in Editor. Please report this issue.") - - if lint_on_save: - var code_edit: CodeEdit = EditorInterface.get_script_editor().get_current_editor().get_base_editor() - var lint_issues := lint_code(script) - if lint_issues.is_empty(): - clear_lint_highlights(code_edit) - else: - apply_lint_highlights(code_edit, lint_issues) - print_lint_summary(lint_issues, script.resource_path) - - -func add_format_command() -> void: - var shortcut := get_editor_setting(SETTING_SHORTCUT) as Shortcut - EditorInterface.get_command_palette().add_command( - COMMAND_PALETTE_FORMAT_SCRIPT, - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_FORMAT_SCRIPT, - format_current_script, - shortcut.get_as_text() if is_instance_valid(shortcut) else "None", - ) - - -func remove_format_command() -> void: - EditorInterface.get_command_palette().remove_command(COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_FORMAT_SCRIPT) - - -func add_lint_command() -> void: - EditorInterface.get_command_palette().add_command( - COMMAND_PALETTE_LINT_SCRIPT, - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_LINT_SCRIPT, - lint_current_script, - ) - - -func remove_lint_command() -> void: - EditorInterface.get_command_palette().remove_command( - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_LINT_SCRIPT, - ) - - -func add_update_command() -> void: - EditorInterface.get_command_palette().add_command( - COMMAND_PALETTE_UPDATE, - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_UPDATE, - # TODO: Implement me - func() -> void: push_error("Update command not implemented!"), - ) - - -func remove_update_command() -> void: - EditorInterface.get_command_palette().remove_command(COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_UPDATE) - - -func add_report_issue_command() -> void: - EditorInterface.get_command_palette().add_command( - COMMAND_PALETTE_REPORT_ISSUE, - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_REPORT_ISSUE, - report_issue, - ) - - -func remove_report_issue_command() -> void: - EditorInterface.get_command_palette().remove_command( - COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_REPORT_ISSUE) - - -func reorder_code() -> bool: - if not EditorInterface.get_script_editor().is_visible_in_tree(): - return false - var current_script := EditorInterface.get_script_editor().get_current_script() - if not is_instance_valid(current_script) or not current_script is GDScript: - return false - var code_edit: CodeEdit = EditorInterface.get_script_editor().get_current_editor().get_base_editor() - - var formatted_code := format_code(current_script, true, code_edit.text) - if formatted_code.is_empty(): - return false - - reload_code_edit(code_edit, formatted_code) - return true - - -func report_issue() -> void: - OS.shell_open("https://github.com/GDQuest/GDScript-formatter/issues") - - -func show_help() -> void: - OS.shell_open("https://www.gdquest.com/library/gdscript_formatter/") - - -func _on_menu_item_selected(command: String) -> void: - match command: - "format_script": - format_current_script() - "lint_script": - lint_current_script() - "reorder_code": - reorder_code() - "update": - # TODO: Implement me - push_error("update not implemented!") - "report_issue": - report_issue() - "help": - show_help() - _: - push_warning("Unsupported command sent from the menu: " + command) - - -## Reloads the code editor with new text while preserving editor state. -## This includes cursor position, scroll position, breakpoints, bookmarks, and folds. -func reload_code_edit( - code_edit: CodeEdit, - new_text: String, - tag_saved := false, -) -> void: - var editor_state := CodeEditState.new(code_edit) - code_edit.text = new_text - if tag_saved: - code_edit.tag_saved_version() - editor_state.restore_to_editor(code_edit) - code_edit.update_minimum_size() - code_edit.text_changed.emit() - - -func get_editor_setting(setting_name: String) -> Variant: - var editor_settings := EditorInterface.get_editor_settings() - var full_setting_key := EDITOR_SETTINGS_CATEGORY + setting_name - if editor_settings.has_setting(full_setting_key): - return editor_settings.get_setting(full_setting_key) - return DEFAULT_SETTINGS[setting_name] - - -func set_editor_setting(setting_name: String, value: Variant) -> void: - var editor_settings := EditorInterface.get_editor_settings() - var full_setting_key := EDITOR_SETTINGS_CATEGORY + setting_name - editor_settings.set_setting(full_setting_key, value) - - -func has_editor_setting(setting_name: String) -> bool: - var editor_settings := EditorInterface.get_editor_settings() - var full_setting_key := EDITOR_SETTINGS_CATEGORY + setting_name - return editor_settings.has_setting(full_setting_key) - - -## Returns true if this script should be formatted automatically on save, based -## on the project's .editorconfig file. Returns false if the config says not to -## format on save. Returns null if no rule matches (then it's the user editor -## settings that take over). -func get_editorconfig_format_on_save(script_path: String) -> Variant: - var editorconfig_path := ProjectSettings.globalize_path("res://.editorconfig") - var modified_time := FileAccess.get_modified_time(editorconfig_path) - if modified_time != _editorconfig_last_modified_time: - _editorconfig_last_modified_time = modified_time - _editorconfig_format_on_save_rules.clear() - load_editorconfig_format_on_save_rules(editorconfig_path) - - var relative_script_path := script_path.trim_prefix("res://") - var format_on_save = null - for rule: Dictionary in _editorconfig_format_on_save_rules: - var pattern := rule["pattern"] as String - if pattern.is_empty() or editorconfig_section_matches(pattern, relative_script_path): - format_on_save = rule["format_on_save"] - - return format_on_save - - -## Loads the project editorconfig file and parses format on save rules. -func load_editorconfig_format_on_save_rules(editorconfig_path: String) -> void: - var editorconfig_file := FileAccess.open(editorconfig_path, FileAccess.READ) - if editorconfig_file == null: - return - - var pattern := "" - - while not editorconfig_file.eof_reached(): - var line := editorconfig_file.get_line().strip_edges() - if line.is_empty() or line.begins_with("#") or line.begins_with(";"): - continue - - if line.begins_with("[") and line.ends_with("]"): - pattern = line.trim_prefix("[").trim_suffix("]") - continue - - if not line.contains("="): - continue - - var key_and_value := line.split("=", true, 1) - if key_and_value[0].strip_edges().to_lower() != "gdscript_formatter_format_on_save": - continue - - match key_and_value[1].strip_edges().to_lower(): - "true": - _editorconfig_format_on_save_rules.append({"pattern": pattern, "format_on_save": true}) - "false": - _editorconfig_format_on_save_rules.append({"pattern": pattern, "format_on_save": false}) - - editorconfig_file.close() - - -## Returns true if an EditorConfig section applies to a saved script. -## pattern: The EditorConfig pattern to match against. -## relative_script_path: The path of the script relative to the project root. -func editorconfig_section_matches(pattern: String, relative_script_path: String) -> bool: - if pattern.is_empty(): - return false - - var normalized_pattern := pattern.trim_prefix("/") - var matching_path := relative_script_path - # If there's no / in the pattern it means this pattern targets a filename. - # It's not a folder/path glob pattern. - if not normalized_pattern.contains("/"): - matching_path = relative_script_path.get_file() - - return matching_path.match(normalized_pattern) - - -## Formats GDScript code using the GDScript Formatter and returns it as a string. -## When source_content is null, reads the code from the GDScript resource directly. -## Otherwise, formats source_content without reading from the file. -## -## Pass a string through source_content when the user is editing the script in -## the editor and requests formatting without having saved their changes (in -## that case, the code they're editing only exists in the script editor's open -## tab). -func format_code(script: GDScript, force_reorder := false, source_content: Variant = null) -> String: - var script_path := script.resource_path - if source_content == null and script_path.is_empty(): - push_error("GDScript Formatter Error: Can't format an unsaved script.") - return "" - - if source_content == null: - source_content = script.source_code - - var printer_config := {} - var formatter_config := { printer = printer_config } - if get_editor_setting(SETTING_USE_SPACES): - printer_config[SETTING_USE_SPACES] = true - printer_config[SETTING_INDENT_SIZE] = get_editor_setting(SETTING_INDENT_SIZE) - if force_reorder or get_editor_setting(SETTING_REORDER_CODE): - formatter_config[SETTING_REORDER_CODE] = true - if get_editor_setting(SETTING_SAFE_MODE): - formatter_config[SETTING_SAFE_MODE] = true - - return GDScriptFormatter.format_gdscript(source_content, formatter_config) - - -## Lints a GDScript file using the GDScript Formatter's linter, -## and returns an array of lint issues. -func lint_code(script: GDScript) -> Array: - if script.resource_path.is_empty(): - push_error("GDScript Formatter Error: Can't format an unsaved script.") - return [] - - var linter_config := { - max_line_length = get_editor_setting(SETTING_LINT_LINE_LENGTH) as int, - disabled_rules = ",".split( - get_editor_setting(SETTING_LINT_IGNORED_RULES) as String), - } - return GDScriptFormatter.lint_gdscript(script.source_code, linter_config) - - -## Applies lint highlighting to the code editor -func apply_lint_highlights(code_edit: CodeEdit, issues: Array) -> void: - clear_lint_highlights(code_edit) - - # Add and set up gutter for lint icons if not already present. - # We check by name to avoid conflicts with gutters added by other addons. - # Once added, the gutter is never removed so the layout doesn't shift on clear. - var has_lint_gutter := false - for i: int in code_edit.get_gutter_count(): - if code_edit.get_gutter_name(i) == GUTTER_LINT_ICONS_NAME: - has_lint_gutter = true - break - if not has_lint_gutter: - code_edit.add_gutter(GUTTER_LINT_ICON_INDEX) - code_edit.set_gutter_name(GUTTER_LINT_ICON_INDEX, GUTTER_LINT_ICONS_NAME) - code_edit.set_gutter_type(GUTTER_LINT_ICON_INDEX, CodeEdit.GutterType.GUTTER_TYPE_ICON) - const EDITOR_ICON_DEFAULT_WIDTH = 16.0 - code_edit.set_gutter_width(GUTTER_LINT_ICON_INDEX, EDITOR_ICON_DEFAULT_WIDTH * EditorInterface.get_editor_scale()) - - for issue in issues: - var line_number: int = issue.line - 1 # Convert to zero-indexed - var severity: String = issue.severity - - var color := Color(1, 0, 0, 0.1) if severity == "error" else Color(1, 1, 0, 0.1) - code_edit.set_line_background_color(line_number, color) - var icon_name := "StatusError" if severity == "error" else "StatusWarning" - var icon := EditorInterface.get_editor_theme().get_icon(icon_name, "EditorIcons") - code_edit.set_line_gutter_icon(line_number, GUTTER_LINT_ICON_INDEX, icon) - - -## Prints a detailed summary of lint issues to the output -func print_lint_summary(issues: Array, script_path: String) -> void: - print_rich("\n[b]=== Linting Results for %s ===[/b]\n" % script_path) - print_rich("[b]Found [i]%s[/i] issue(s)\n[/b]" % issues.size()) - - for issue in issues: - var line_display = str(issue.line) - var severity_label = issue.severity.to_upper() - print_rich( - "[color=%s]%s[/color] on line [color=cyan]%s[/color] ([i]%s[/i])" % [ - "red" if severity_label == "ERROR" else "yellow", - severity_label, - line_display, - issue.rule, - ], - ) - print_rich("[i]%s[/i]\n" % [issue.message]) - - print_rich("[b]=== End Linting Results ===[/b]\n") - - -## Clears all lint highlighting from the code editor. -## The lint gutter is intentionally kept so the layout does not shift. -func clear_lint_highlights(code_edit: CodeEdit) -> void: - var lint_gutter_index := -1 - for i: int in code_edit.get_gutter_count(): - if code_edit.get_gutter_name(i) == GUTTER_LINT_ICONS_NAME: - lint_gutter_index = i - break - - for line in range(code_edit.get_line_count()): - code_edit.set_line_background_color(line, Color(0, 0, 0, 0)) - if lint_gutter_index != -1: - code_edit.set_line_gutter_icon(line, lint_gutter_index, null) - - -## Data structure to hold code editor state information -class CodeEditState: - var caret_line: int - var caret_column: int - var horizontal_scroll: int - var vertical_scroll: int - var breakpoints: Dictionary[int, String] = { } - var bookmarks: Dictionary[int, String] = { } - var folds: Dictionary[int, String] = { } - var code_edit: CodeEdit - - - func _init(code_edit: CodeEdit) -> void: - self.code_edit = code_edit - caret_line = code_edit.get_caret_line() - caret_column = code_edit.get_caret_column() - horizontal_scroll = code_edit.scroll_horizontal - vertical_scroll = code_edit.scroll_vertical - - for line in code_edit.get_breakpointed_lines(): - breakpoints[line] = code_edit.get_line(line) - for line in code_edit.get_bookmarked_lines(): - bookmarks[line] = code_edit.get_line(line) - for line in code_edit.get_folded_lines(): - folds[line] = code_edit.get_line(line) - - - func restore_to_editor(code_edit: CodeEdit) -> void: - var new_line_count := code_edit.get_line_count() - - _restore_line_features(breakpoints, code_edit.set_line_as_breakpoint, new_line_count) - _restore_line_features(bookmarks, code_edit.set_line_as_bookmarked, new_line_count) - _restore_line_features(folds, func(line: int, _is_folded: bool) -> void: code_edit.fold_line(line), new_line_count) - - code_edit.set_caret_line(caret_line) - code_edit.set_caret_column(caret_column) - - code_edit.scroll_horizontal = horizontal_scroll - code_edit.scroll_vertical = vertical_scroll - - - ## Restores line-based features (breakpoints, bookmarks, folds) by finding the best matching lines - ## in the new text based on similarity to the original line text. - ## - ## Big thanks to https://github.com/Daylily-Zeleen/GDScript-Formatter for this approach. - func _restore_line_features( - stored_features: Dictionary, - set_line_func: Callable, - new_line_count: int, - ) -> void: - var stored_lines := PackedInt64Array(stored_features.keys()) - for line_index in range(stored_lines.size()): - var original_line := stored_lines[line_index] as int - var original_text := stored_features[original_line] as String - - # After formatting lines can move, so we need to find the best match for the original line - # to restore the breakpoints, bookmarks, and folds. - # We first check the same line, then we expand our search outwards until we find a match. - # We use a similarity threshold of 0.9 to account for minor changes in the line text. - # This should be sufficient for most cases, but might need adjustment for edge cases. - # If no match is found, we skip restoring this feature - if original_line < new_line_count and code_edit.get_line(original_line).similarity(original_text) > 0.9: - set_line_func.call(original_line, true) - continue - - var line_above := original_line - 1 - var line_below := original_line + 1 - while line_above >= 0 or line_below < new_line_count: - if line_below < new_line_count and code_edit.get_line(line_below).similarity(original_text) > 0.9: - set_line_func.call(line_below, true) - break - if line_above >= 0 and code_edit.get_line(line_above).similarity(original_text) > 0.9: - set_line_func.call(line_above, true) - break - - line_above -= 1 - line_below += 1 diff --git a/addons/GDQuest_GDScript_formatter_standalone/plugin.gd.uid b/addons/GDQuest_GDScript_formatter_standalone/plugin.gd.uid deleted file mode 100644 index fcc257d..0000000 --- a/addons/GDQuest_GDScript_formatter_standalone/plugin.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ikxq6eiwl3ey