Skip to content

Commit 2300f33

Browse files
committed
Godot addon: Add support for format on save
Close #181
1 parent 250ac80 commit 2300f33

2 files changed

Lines changed: 94 additions & 6 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ To see other possible options, run `gdscript-formatter --help`.
8181

8282
You can also configure the formatter with an [EditorConfig](https://editorconfig.org/) file at the root of your project. This is a good way to share the same formatting settings with your whole team. The formatter supports the standard keys `indent_style`, `indent_size`, `max_line_length`, `insert_final_newline`, and `trim_trailing_whitespace`, plus custom keys prefixed with `gdscript_formatter_`. See the [GDScript Formatter docs](https://www.gdquest.com/library/gdscript_formatter/) for the complete list. Note that command line flags override `.editorconfig` values.
8383

84+
The Godot add-on also reads `gdscript_formatter_format_on_save` from the `.editorconfig` file. This key only affects the add-on and enables or disables format on save for the whole project and overrides each user's add-on setting. For example:
85+
86+
```ini
87+
[*.gd]
88+
gdscript_formatter_format_on_save = true
89+
```
90+
91+
Use this to force everyone in your team to format their GDScript files on save.
92+
8493
To keep a section of code exactly as you wrote it, wrap it between `# fmt: off` and `# fmt: on` comments. This is especially useful when you want to keep values aligned or arranged in a specific way when long data structures:
8594

8695
```gdscript

addons/GDQuest_GDScript_formatter/plugin.gd

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ var installer: FormatterInstaller = null
5555
var formatter_cache_dir: String
5656
var menu: FormatterMenu = null
5757
var _has_uninstall_command := false
58+
# Used to auto detect changes to the project's .editorconfig file.
59+
var _editorconfig_last_modified_time := -1
60+
# Editorconfig allows setting rules per path glob. We track globs for the format
61+
# on save rule here so users can enable it selectively for specific folders.
62+
var _editorconfig_format_on_save_rules: Array[Dictionary] = []
5863

5964

6065
func _init() -> void:
@@ -197,15 +202,17 @@ func _on_resource_saved(saved_resource: Resource) -> void:
197202
if saved_resource is not GDScript:
198203
return
199204

200-
var format_on_save := get_editor_setting(SETTING_FORMAT_ON_SAVE) as bool
205+
var script := saved_resource as GDScript
206+
var do_format_on_save := get_editor_setting(SETTING_FORMAT_ON_SAVE) as bool
207+
var editorconfig_format_on_save = get_editorconfig_format_on_save(script.resource_path)
208+
if editorconfig_format_on_save != null:
209+
do_format_on_save = editorconfig_format_on_save as bool
201210
var lint_on_save := get_editor_setting(SETTING_LINT_ON_SAVE) as bool
202211

203-
if not format_on_save and not lint_on_save:
212+
if not do_format_on_save and not lint_on_save:
204213
return
205214

206-
var script := saved_resource as GDScript
207-
208-
var ignored_directories := get_editor_setting(SETTING_IGNORED_DIRECTORIES)
215+
var ignored_directories = get_editor_setting(SETTING_IGNORED_DIRECTORIES)
209216
var path = script.resource_path.trim_prefix("res://")
210217

211218
var script_path_parts := path.split("/")
@@ -226,7 +233,7 @@ func _on_resource_saved(saved_resource: Resource) -> void:
226233
if not has_command(get_editor_setting(SETTING_FORMATTER_PATH)) or not is_instance_valid(script):
227234
return
228235

229-
if format_on_save:
236+
if do_format_on_save:
230237
var formatted_code := format_code(script, false)
231238
if formatted_code.is_empty():
232239
return
@@ -465,6 +472,78 @@ func has_editor_setting(setting_name: String) -> bool:
465472
return editor_settings.has_setting(full_setting_key)
466473

467474

475+
## Returns true if this script should be formatted automatically on save, based
476+
## on the project's .editorconfig file. Returns false if the config says not to
477+
## format on save. Returns null if no rule matches (then it's the user editor
478+
## settings that take over).
479+
func get_editorconfig_format_on_save(script_path: String) -> Variant:
480+
var editorconfig_path := ProjectSettings.globalize_path("res://.editorconfig")
481+
var modified_time := FileAccess.get_modified_time(editorconfig_path)
482+
if modified_time != _editorconfig_last_modified_time:
483+
_editorconfig_last_modified_time = modified_time
484+
_editorconfig_format_on_save_rules.clear()
485+
load_editorconfig_format_on_save_rules(editorconfig_path)
486+
487+
var relative_script_path := script_path.trim_prefix("res://")
488+
var format_on_save = null
489+
for rule: Dictionary in _editorconfig_format_on_save_rules:
490+
var pattern := rule["pattern"] as String
491+
if pattern.is_empty() or editorconfig_section_matches(pattern, relative_script_path):
492+
format_on_save = rule["format_on_save"]
493+
494+
return format_on_save
495+
496+
497+
## Loads the project editorconfig file and parses format on save rules.
498+
func load_editorconfig_format_on_save_rules(editorconfig_path: String) -> void:
499+
var editorconfig_file := FileAccess.open(editorconfig_path, FileAccess.READ)
500+
if editorconfig_file == null:
501+
return
502+
503+
var pattern := ""
504+
505+
while not editorconfig_file.eof_reached():
506+
var line := editorconfig_file.get_line().strip_edges()
507+
if line.is_empty() or line.begins_with("#") or line.begins_with(";"):
508+
continue
509+
510+
if line.begins_with("[") and line.ends_with("]"):
511+
pattern = line.trim_prefix("[").trim_suffix("]")
512+
continue
513+
514+
if not line.contains("="):
515+
continue
516+
517+
var key_and_value := line.split("=", true, 1)
518+
if key_and_value[0].strip_edges().to_lower() != "gdscript_formatter_format_on_save":
519+
continue
520+
521+
match key_and_value[1].strip_edges().to_lower():
522+
"true":
523+
_editorconfig_format_on_save_rules.append({"pattern": pattern, "format_on_save": true})
524+
"false":
525+
_editorconfig_format_on_save_rules.append({"pattern": pattern, "format_on_save": false})
526+
527+
editorconfig_file.close()
528+
529+
530+
## Returns true if an EditorConfig section applies to a saved script.
531+
## pattern: The EditorConfig pattern to match against.
532+
## relative_script_path: The path of the script relative to the project root.
533+
func editorconfig_section_matches(pattern: String, relative_script_path: String) -> bool:
534+
if pattern.is_empty():
535+
return false
536+
537+
var normalized_pattern := pattern.trim_prefix("/")
538+
var matching_path := relative_script_path
539+
# If there's no / in the pattern it means this pattern targets a filename.
540+
# It's not a folder/path glob pattern.
541+
if not normalized_pattern.contains("/"):
542+
matching_path = relative_script_path.get_file()
543+
544+
return matching_path.match(normalized_pattern)
545+
546+
468547
## Formats GDScript code using the GDScript Formatter and returns it as a string.
469548
## When source_content is null, reads the code from the GDScript resource directly.
470549
## Otherwise, formats source_content without reading from the file.

0 commit comments

Comments
 (0)