├── LICENSE ├── README.md ├── addons └── diff-margin │ ├── README.md │ ├── plugin.cfg │ ├── plugin.gd │ ├── popup.gd │ ├── popup.tscn │ ├── screenshot1.png │ └── screenshot1.png.import └── icon.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Cédric Coulon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diff margin 2 | 3 | Diff Margin is a Godot plugin. Please see official website for [instructions to install](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/installing_plugins.html). 4 | 5 | Diff Margin displays Git changes of the currently edited file on Godot script editor margin. 6 | 7 | Customization: 8 | - Git binary path 9 | - Gutter width 10 | - Gutter colors 11 | 12 | Tested on Godot 4.3 Windows only. 13 | 14 | ![Editor](addons/diff-margin/screenshot1.png) 15 | -------------------------------------------------------------------------------- /addons/diff-margin/README.md: -------------------------------------------------------------------------------- 1 | # Diff margin 2 | 3 | Diff Margin is a Godot plugin. Please see official website for [instructions to install](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/installing_plugins.html). 4 | 5 | Diff Margin displays Git changes of the currently edited file on Godot script editor margin. 6 | 7 | Customization: 8 | - Git binary path 9 | - Gutter width 10 | - Gutter colors 11 | 12 | Tested on Godot 4.3 Windows only. 13 | 14 | All settings can be changed in the `Editor Settings` under `Plugin` -> `Diff margin` 15 | 16 | ![Editor](screenshot1.png) 17 | -------------------------------------------------------------------------------- /addons/diff-margin/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="diff-margin" 4 | description="Diff Margin displays Git changes of the currently edited file on Godot script editor margin" 5 | author="Datoh" 6 | version="0.2" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/diff-margin/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const DELETE_TOP = 1 5 | const DELETE_BOTTOM = 2 6 | 7 | const POPUP_DIFF = preload("res://addons/diff-margin/popup.tscn") 8 | 9 | ## Editor setting path 10 | const DIFF_MARGIN: StringName = &"plugin/diff-margin/" 11 | const GIT_PATH: StringName = DIFF_MARGIN + &"git_path" 12 | const GUTTER_WIDTH: StringName = DIFF_MARGIN + &"gutter_width" 13 | const COLOR_DELETE: StringName = DIFF_MARGIN + &"color_delete" 14 | const COLOR_ADD: StringName = DIFF_MARGIN + &"color_add" 15 | const COLOR_REPLACE: StringName = DIFF_MARGIN + &"color_replace" 16 | 17 | var gutter_width := 0 18 | var git_path := "" 19 | var color_delete := Color() 20 | var color_add := Color() 21 | var color_replace := Color() 22 | 23 | var _editor: CodeEdit = null 24 | var _gutter_id := -1 25 | var _diffs: Array[Array] = [] # [[start, count, old_content]] 26 | var _diffs_map: = {} # 27 | var _popup_diff: PopupPanel = null 28 | 29 | func _enter_tree() -> void: 30 | var editor_settings := EditorInterface.get_editor_settings() 31 | editor_settings.settings_changed.connect(_on_settings_changed) 32 | if not editor_settings.has_setting(GIT_PATH): 33 | editor_settings.set_settings(GIT_PATH, "") 34 | editor_settings.set_initial_value(GIT_PATH, "", false) 35 | if not editor_settings.has_setting(GUTTER_WIDTH): 36 | editor_settings.set_settings(GUTTER_WIDTH, 6) 37 | editor_settings.set_initial_value(GUTTER_WIDTH, 6, false) 38 | if not editor_settings.has_setting(COLOR_DELETE): 39 | editor_settings.set_settings(COLOR_DELETE, Color.PALE_VIOLET_RED) 40 | editor_settings.set_initial_value(COLOR_DELETE, Color.PALE_VIOLET_RED, false) 41 | if not editor_settings.has_setting(COLOR_ADD): 42 | editor_settings.set_settings(COLOR_ADD, Color.LIGHT_GREEN) 43 | editor_settings.set_initial_value(COLOR_ADD, Color.LIGHT_GREEN, false) 44 | if not editor_settings.has_setting(COLOR_REPLACE): 45 | editor_settings.set_settings(COLOR_REPLACE, Color.SKY_BLUE) 46 | editor_settings.set_initial_value(COLOR_REPLACE, Color.SKY_BLUE, false) 47 | git_path = editor_settings.get_setting(GIT_PATH) 48 | gutter_width = editor_settings.get_setting(GUTTER_WIDTH) 49 | color_delete = editor_settings.get_setting(COLOR_DELETE) 50 | color_add = editor_settings.get_setting(COLOR_ADD) 51 | color_replace = editor_settings.get_setting(COLOR_REPLACE) 52 | 53 | var script_editor := EditorInterface.get_script_editor() 54 | script_editor.editor_script_changed.connect(_on_editor_script_changed) 55 | script_editor.focus_entered.connect(_on_editor_script_focus_entered) 56 | _on_editor_script_changed() 57 | resource_saved.connect(_on_resource_saved) 58 | 59 | _popup_diff = POPUP_DIFF.instantiate() 60 | _popup_diff.undo.connect(_on_undo_diff) 61 | script_editor.add_child(_popup_diff) 62 | _popup_diff.visible = false 63 | 64 | 65 | func _exit_tree() -> void: 66 | var editor_settings := EditorInterface.get_editor_settings() 67 | editor_settings.settings_changed.disconnect(_on_settings_changed) 68 | var script_editor = EditorInterface.get_script_editor() 69 | script_editor.editor_script_changed.disconnect(_on_editor_script_changed) 70 | resource_saved.disconnect(_on_resource_saved) 71 | if _gutter_id != -1: 72 | _editor.gutter_clicked.disconnect(_on_gutter_clicked) 73 | _editor.remove_gutter(_gutter_id) 74 | script_editor.remove_child(_popup_diff) 75 | _popup_diff.free() 76 | 77 | 78 | func _on_settings_changed(): 79 | var editor_settings := EditorInterface.get_editor_settings() 80 | var changed_settings: PackedStringArray = editor_settings.get_changed_settings() 81 | for setting: String in changed_settings: 82 | if (!setting.begins_with(DIFF_MARGIN)): 83 | continue 84 | if setting == GIT_PATH: 85 | git_path = editor_settings.get_setting(setting) 86 | elif setting == GUTTER_WIDTH: 87 | gutter_width = editor_settings.get_setting(setting) 88 | elif setting == COLOR_DELETE: 89 | color_delete = editor_settings.get_setting(setting) 90 | elif setting == COLOR_ADD: 91 | color_add = editor_settings.get_setting(setting) 92 | elif setting == COLOR_REPLACE: 93 | color_replace = editor_settings.get_setting(setting) 94 | 95 | 96 | func _on_resource_saved(resource: Resource): 97 | if resource is Script: 98 | _on_editor_script_changed(resource as Script) 99 | 100 | 101 | func _on_editor_script_focus_entered(): 102 | _on_editor_script_changed() 103 | 104 | 105 | func _on_editor_script_changed(_script: Script = null): 106 | if _gutter_id != -1: 107 | _editor.gutter_clicked.disconnect(_on_gutter_clicked) 108 | _editor.remove_gutter(_gutter_id) 109 | 110 | var script_editor = EditorInterface.get_script_editor() 111 | if not script_editor or not script_editor.get_current_editor(): 112 | return 113 | _editor = script_editor.get_current_editor().get_base_editor() 114 | _gutter_id = _editor.get_gutter_count() 115 | _editor.gutter_clicked.connect(_on_gutter_clicked) 116 | _editor.add_gutter() 117 | _editor.set_gutter_type(_gutter_id, TextEdit.GUTTER_TYPE_CUSTOM) 118 | _editor.set_gutter_custom_draw(_gutter_id, _on_gutter_custom_draw) 119 | _editor.set_gutter_width(_gutter_id, gutter_width) 120 | 121 | _diffs.clear() 122 | _diffs_map.clear() 123 | 124 | if git_path.is_empty(): 125 | printerr("Git path is not defined (Editor > Editor Settings... > Plugin > Diff-margin)") 126 | 127 | var path = script_editor.get_current_script().get_path().substr(6, -1) 128 | var result = [] 129 | 130 | # check if file is untracked 131 | var exit_code = OS.execute(git_path, ["status", "-s", path], result) 132 | if exit_code != 0: 133 | if not git_path.is_empty(): 134 | printerr("Check the git path (Editor > Editor Settings... > Plugin > Diff-margin)") 135 | return 136 | 137 | if exit_code == 0 and not result.is_empty() and result[0].substr(0, 2) == "??": 138 | _apply_diff(0, _editor.get_line_count(), 0, "") 139 | return 140 | 141 | result.clear() 142 | exit_code = OS.execute(git_path, ["diff", "-U0", path], result) 143 | var diffs_string = [] if result.size() != 1 or result[0].is_empty() else result[0].split("\n").slice(4, -1) 144 | var removed := false 145 | var start := -1 146 | var count := 0 147 | var old_content := "" 148 | for diff_string: String in diffs_string: 149 | var first_char := diff_string[0] 150 | if first_char == "@": 151 | if start >= 0: 152 | _apply_diff(start, count, removed, old_content) 153 | var array = diff_string.split(" ") 154 | var removes = array[1].substr(1).split(",") 155 | var adds = array[2].substr(1).split(",") 156 | start = int(adds[0]) - 1 157 | count = int(adds[1]) if adds.size() > 1 else 1 158 | removed = (int(removes[1]) if removes.size() > 1 else 1) != 0 159 | old_content = "" 160 | elif first_char == "-": 161 | old_content = diff_string.substr(1) if old_content.is_empty() else old_content + "\n" + diff_string.substr(1) 162 | if start >= 0: 163 | _apply_diff(start, count, removed, old_content) 164 | 165 | 166 | func _apply_diff(start: int, count: int, removed: bool, old_content: String): 167 | var diff_index := _diffs.size() 168 | if not old_content.is_empty(): 169 | old_content += "\n" 170 | var line_count := _editor.get_line_count() 171 | var is_remove_only := count == 0 and removed 172 | var is_add_only := count > 0 and not removed 173 | var end := start + 2 if is_remove_only else start + count 174 | var color := color_add if is_add_only else color_replace 175 | color = color_delete if is_remove_only else color 176 | _diffs.append([start + 1 if is_remove_only else start, count, old_content]) 177 | for line in range(start, end): 178 | if line >= 0 and line < line_count: 179 | _editor.set_line_gutter_item_color(line, _gutter_id, color) 180 | _editor.set_line_gutter_clickable(line, _gutter_id, true) 181 | if is_remove_only: 182 | _editor.set_line_gutter_metadata(line, _gutter_id, DELETE_TOP if line == start else DELETE_BOTTOM) 183 | _diffs_map[line] = diff_index 184 | 185 | 186 | func _on_gutter_clicked(line: int, gutter: int): 187 | if gutter != _gutter_id or not _editor.is_line_gutter_clickable(line, gutter): 188 | return 189 | 190 | _popup_diff.line = line 191 | _popup_diff.content = _diffs[_diffs_map[line]][2] 192 | _popup_diff.popup(Rect2i(get_viewport().get_mouse_position(), Vector2.ZERO)) 193 | 194 | 195 | func _on_undo_diff(line: int): 196 | var start: int = _diffs[_diffs_map[line]][0] 197 | var count: int = _diffs[_diffs_map[line]][1] 198 | var old_content: String = _diffs[_diffs_map[line]][2] 199 | _editor.remove_text(start, 0, start + count, 0) 200 | _editor.insert_text(old_content, start, 0) 201 | EditorInterface.save_scene() 202 | 203 | 204 | func _on_gutter_custom_draw(line: int, gutter: int, area: Rect2): 205 | if gutter != _gutter_id or not _editor.is_line_gutter_clickable(line, gutter): 206 | return 207 | var color := _editor.get_line_gutter_item_color(line, gutter) 208 | var metadata = _editor.get_line_gutter_metadata(line, _gutter_id) 209 | if metadata == null: 210 | _editor.draw_rect(area, color) 211 | elif metadata == DELETE_TOP: 212 | _editor.draw_colored_polygon(PackedVector2Array([Vector2(area.position.x, area.end.y - gutter_width), area.end, Vector2(area.position.x, area.end.y)]), color) 213 | elif metadata == DELETE_BOTTOM: 214 | _editor.draw_colored_polygon(PackedVector2Array([area.position, Vector2(area.end.x, area.position.y), Vector2(area.position.x, area.position.y + gutter_width)]), color) 215 | -------------------------------------------------------------------------------- /addons/diff-margin/popup.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends PopupPanel 3 | 4 | var line := -1 5 | var content := "" 6 | 7 | signal undo(line: int) 8 | signal hide_popup() 9 | 10 | func _on_undo_button_pressed() -> void: 11 | undo.emit(line) 12 | 13 | 14 | func _on_copy_button_pressed() -> void: 15 | if not content.is_empty(): 16 | DisplayServer.clipboard_set(content) 17 | 18 | 19 | func _on_about_to_popup() -> void: 20 | # remove old label 21 | while %VBoxContainer.get_child_count() > 1: 22 | %VBoxContainer.remove_child(%VBoxContainer.get_child(1)) 23 | # force recompute size 24 | size = Vector2(100, 100) 25 | if content.is_empty(): 26 | return 27 | var label = Label.new() 28 | label.text = content 29 | %VBoxContainer.add_child(label) 30 | -------------------------------------------------------------------------------- /addons/diff-margin/popup.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://btdve3nxtdb1v"] 2 | 3 | [ext_resource type="Script" path="res://addons/diff-margin/popup.gd" id="1_jfria"] 4 | 5 | [node name="PopupPanel" type="PopupPanel"] 6 | size = Vector2i(109, 100) 7 | visible = true 8 | script = ExtResource("1_jfria") 9 | 10 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 11 | unique_name_in_owner = true 12 | offset_left = 4.0 13 | offset_top = 4.0 14 | offset_right = 105.0 15 | offset_bottom = 96.0 16 | 17 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] 18 | layout_mode = 2 19 | 20 | [node name="UndoButton" type="Button" parent="VBoxContainer/HBoxContainer"] 21 | layout_mode = 2 22 | theme_override_font_sizes/font_size = 10 23 | text = "Undo" 24 | 25 | [node name="CopyButton" type="Button" parent="VBoxContainer/HBoxContainer"] 26 | layout_mode = 2 27 | theme_override_font_sizes/font_size = 10 28 | text = "Copy" 29 | 30 | [connection signal="about_to_popup" from="." to="." method="_on_about_to_popup"] 31 | [connection signal="pressed" from="VBoxContainer/HBoxContainer/UndoButton" to="." method="_on_undo_button_pressed"] 32 | [connection signal="pressed" from="VBoxContainer/HBoxContainer/CopyButton" to="." method="_on_copy_button_pressed"] 33 | -------------------------------------------------------------------------------- /addons/diff-margin/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Datoh/godot-diff-margin/0679b32f62a3bfee80930a94825171db11bcd04c/addons/diff-margin/screenshot1.png -------------------------------------------------------------------------------- /addons/diff-margin/screenshot1.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dlc0phaeu41u1" 6 | path="res://.godot/imported/screenshot1.png-43cc0d0acff28f7327ed600f9a75c242.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/diff-margin/screenshot1.png" 14 | dest_files=["res://.godot/imported/screenshot1.png-43cc0d0acff28f7327ed600f9a75c242.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Datoh/godot-diff-margin/0679b32f62a3bfee80930a94825171db11bcd04c/icon.png --------------------------------------------------------------------------------