加两个插件
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user