Adding FormatOnSave

This commit is contained in:
Lucas 2025-07-17 17:52:00 +02:00
parent f8b8370183
commit 2e7c67b0ef
No known key found for this signature in database
12 changed files with 383 additions and 0 deletions

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025-present VitSoonYoung - <vitsoonyoung+simpleformatonsave@gmail.com>
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.

View file

@ -0,0 +1,10 @@
class_name Formatter
const RuleSpacing = preload("./rules/spacing.gd")
const RuleBlankLines = preload("./rules/blank_lines.gd")
func format_code(code: String) -> String:
code = RuleSpacing.apply(code)
code = RuleBlankLines.apply(code)
return code

View file

@ -0,0 +1 @@
uid://clqnxlippxlm4

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cv42njjkdhl2l"
path="res://.godot/imported/icon.png-c0076118964b028971c5407cf9f25f2c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Addons/SimpleFormatOnSave/icon.png"
dest_files=["res://.godot/imported/icon.png-c0076118964b028971c5407cf9f25f2c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,7 @@
[plugin]
name="Simple Format On Save"
description="Combination of simple gdscript formatter & Format On Save with extra rules for beatiful formatting...maybe"
author="VitSoonYoung"
version="0.1"
script="simple_format_on_save.gd"

View file

@ -0,0 +1,64 @@
class_name RuleBlankLines
var FORMAT_ACTION := "simple_format_on_save/format_code"
var format_key: InputEventKey
static func apply(code: String) -> String:
var trim1_regex = RegEx.create_from_string("\n{2,}")
code = trim1_regex.sub(code, "\n\n", true)
code = _blank_for_func_class(code)
var trim2_regex = RegEx.create_from_string("\n{3,}")
code = trim2_regex.sub(code, "\n\n\n", true)
return code
static func _blank_for_func_class(code: String) -> String:
var assignment_regex = RegEx.create_from_string(r".*=.*")
var statement_regex = RegEx.create_from_string(r"\s+(if|for|while|match)[\s|\(].*")
var misc_statement_regex = RegEx.create_from_string(r"\s+(else|elif|\}|\]).*")
var func_class_regex = RegEx.create_from_string(r".*(func|class) .*")
var comment_line_regex = RegEx.create_from_string(r"^\s*#")
var empty_line_regex = RegEx.create_from_string(r"^\s+$")
var lines := code.split('\n')
var modified_lines: Array[String] = []
for line: String in lines:
# Spaces between functions & classes
if func_class_regex.search(line):
if modified_lines.size() > 0:
var i := modified_lines.size() - 1
while i > 0 and comment_line_regex.search(modified_lines[i]):
i -= 1
if i == 0:
modified_lines.append(line)
continue
modified_lines.insert(i + 1, "")
modified_lines.insert(i + 1, "")
# 1 space between assignment & statement
if statement_regex.search(line):
if modified_lines.size() > 0:
var i := modified_lines.size() - 1
if assignment_regex.search(modified_lines[i]) and not statement_regex.search(modified_lines[i]) and not statement_regex.search(line):
modified_lines.insert(i + 1, "")
else:
pass
# Space after a code block (Doesn't work with spaces for now)
var indent_count := line.count("\t")
if indent_count and not misc_statement_regex.search(line) and not statement_regex.search(line):
if modified_lines.size() > 0:
var i := modified_lines.size() - 1
if modified_lines[i].count("\t") > indent_count:
modified_lines.insert(i + 1, "")
modified_lines.append(line)
return "\n".join(modified_lines)

View file

@ -0,0 +1 @@
uid://dr266abkfhg1o

View file

@ -0,0 +1,155 @@
class_name RuleSpacing
const SYMBOLS = [
r"\*\*=",
r"\*\*",
"<<=",
">>=",
"<<",
">>",
"==",
"!=",
">=",
"<=",
"&&",
r"\|\|",
r"\+=",
"-=",
r"\*=",
"/=",
"%=",
"&=",
r"\^=",
r"\|=",
"~=",
":=",
"->",
r"&",
r"\|",
r"\^",
"-",
r"\+",
"/",
r"\*",
">",
"<",
"-",
"%",
"=",
":",
",",
];
const KEYWORDS = [
"and",
"is",
"or",
"not",
]
static func apply(code: String) -> String:
var string_regex = RegEx.new()
string_regex.compile(r'(?<!#)("""|\'\'\'|"|\')((?:.|\n)*?)\1')
var string_matches = string_regex.search_all(code)
var string_map = {}
for i in range(string_matches.size()):
var match = string_matches[i]
var original = match.get_string()
var placeholder = "__STRING__%d__" % i
string_map[placeholder] = original
code = _replace(code, original, placeholder)
var comment_regex = RegEx.new()
comment_regex.compile("#.*")
var comment_matches = comment_regex.search_all(code)
var comment_map = {}
for i in range(comment_matches.size()):
var match = comment_matches[i]
var original = match.get_string()
var placeholder = "__COMMENT__%d__" % i
comment_map[placeholder] = original
code = _replace(code, original, placeholder)
var ref_regex = RegEx.new()
ref_regex.compile(r"\$[^.]*")
var ref_matches = ref_regex.search_all(code)
var ref_map = {}
for i in range(ref_matches.size()):
var match = ref_matches[i]
var original = match.get_string()
var placeholder = "__REF__%d__" % i
ref_map[placeholder] = original
code = _replace(code, original, placeholder)
code = _format_operators_and_commas(code)
for placeholder in ref_map:
code = code.replace(placeholder, ref_map[placeholder])
for placeholder in comment_map:
code = code.replace(placeholder, comment_map[placeholder])
for placeholder in string_map:
code = code.replace(placeholder, string_map[placeholder])
return code
static func _format_operators_and_commas(code: String) -> String:
var indent_regex = RegEx.create_from_string(r"^\s{4}")
var new_code = indent_regex.sub(code, "\t", true)
while (code != new_code):
code = new_code
new_code = indent_regex.sub(code, "\t", true)
var symbols_regex = "(" + ") | (".join(SYMBOLS) + ")"
symbols_regex = " * ?(" + symbols_regex + ") * "
var symbols_operator_regex = RegEx.create_from_string(symbols_regex)
code = symbols_operator_regex.sub(code, " $1 ", true)
# ": =" => ":="
code = RegEx.create_from_string(r": *=").sub(code, ":=", true)
# "a(" => "a ("
code = RegEx.create_from_string(r"(?<=[\w\)\]]) *([\(:,])(?!=)").sub(code, "$1", true)
# "( a" => "(a"
code = RegEx.create_from_string(r"([\(\{}]) *").sub(code, "$1", true)
# "a )" => "a)"
code = RegEx.create_from_string(r" *([\)\}])").sub(code, "$1", true)
# "if(" => "if ("
code = RegEx.create_from_string(r"\b(if|for|while|switch|match)\(").sub(code, "$1 (", true)
var keywoisrd_regex = r"|".join(KEYWORDS)
var keyword_operator_regex = RegEx.create_from_string(r"(?<=[ \)\]])(" + keywoisrd_regex + r")(?=[ \(\[])")
code = keyword_operator_regex.sub(code, " $1 ", true)
# tab "a\t=" => "a ="
code = RegEx.create_from_string(r"(\t*. * ?)\t * ").sub(code, "$1", true)
#trim
code = RegEx.create_from_string("[ \t]*\n").sub(code, "\n", true)
# " " => " "
code = RegEx.create_from_string(" +").sub(code, " ", true)
# "= -a" => "= -a"
code = RegEx.create_from_string(r"([=,(] ?)- ").sub(code, "$1-", true)
return code
static func _replace(text: String, what: String, forwhat: String) -> String:
var index := text.find(what)
if index != -1:
text = text.substr(0, index) + forwhat + text.substr(index + what.length())
return text

View file

@ -0,0 +1 @@
uid://wfodrj6vpfb6

View file

@ -0,0 +1,82 @@
@tool
extends EditorPlugin
var FORMAT_ACTION := "simple_format_on_save/format_code"
var format_key: InputEventKey
var formatter: Formatter
func _enter_tree():
add_tool_menu_item("Format (Ctrl+Alt+L)", _on_format_code)
if InputMap.has_action(FORMAT_ACTION):
InputMap.erase_action(FORMAT_ACTION)
InputMap.add_action(FORMAT_ACTION)
format_key = InputEventKey.new()
format_key.keycode = KEY_L
format_key.ctrl_pressed = true
format_key.alt_pressed = true
InputMap.action_add_event(FORMAT_ACTION, format_key)
resource_saved.connect(on_resource_saved)
func _exit_tree():
remove_tool_menu_item("Format (Ctrl+Alt+L)")
InputMap.erase_action(FORMAT_ACTION)
resource_saved.disconnect(on_resource_saved)
# Return true if formatted code != original code
func _on_format_code() -> bool:
var current_editor := EditorInterface.get_script_editor().get_current_editor()
if not (current_editor and current_editor.is_class("ScriptTextEditor")):
return false
var text_edit = current_editor.get_base_editor()
var code = text_edit.text
if not formatter:
formatter = Formatter.new()
var formatted_code = formatter.format_code(code)
if not formatted_code:
return false
if code.length() == formatted_code.length() and code == formatted_code:
return false
var scroll_horizontal = text_edit.scroll_horizontal
var scroll_vertical = text_edit.scroll_vertical
var caret_column = text_edit.get_caret_column(0)
var caret_line = text_edit.get_caret_line(0)
text_edit.text = formatted_code
text_edit.set_caret_line(caret_line)
text_edit.set_caret_column(caret_column)
text_edit.scroll_horizontal = scroll_horizontal
text_edit.scroll_vertical = scroll_vertical
return true
func _shortcut_input(event: InputEvent) -> void:
if event is InputEventKey and event.is_pressed():
if Input.is_action_pressed(FORMAT_ACTION):
_on_format_code()
# CALLED WHEN A SCRIPT IS SAVED
func on_resource_saved(resource: Resource):
if resource is Script:
var script: Script = resource
var current_script = get_editor_interface().get_script_editor().get_current_script()
# Prevents other unsaved scripts from overwriting the active one
if current_script == script:
var is_modified: bool = _on_format_code()
#if is_modified:
#print_rich("[color=#636363]Auto formatted code[/color]")

View file

@ -0,0 +1 @@
uid://b6d7va37k5131