加两个插件

This commit is contained in:
yvmou_
2026-09-12 02:07:17 +08:00
parent 9bb2f9c87b
commit a02116a206
57 changed files with 5894 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
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.
@@ -0,0 +1,240 @@
@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
@@ -0,0 +1 @@
uid://ccblhowfwltqi
+166
View File
@@ -0,0 +1,166 @@
## 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)
@@ -0,0 +1 @@
uid://bqwfn5bsuds1m
@@ -0,0 +1,7 @@
[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"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://dqgr353xopp3y