Files
2026-07-19 10:47:28 +02:00

395 lines
11 KiB
GDScript

@tool
class_name YAMLCodeEditor extends CodeEdit
signal content_changed
signal save_requested
signal close_requested
signal undo_requested
signal redo_requested
signal validation_requested
signal zoom_changed(zoom_level)
var error_indicators := {}
var snapshot_debounce_timer: Timer
var error_line_color: Color = Color(1.0, 0.3, 0.3, 0.1)
var syntax_highlighter_script = preload("res://addons/yaml/editor/syntax_highlighting/editor_syntax_highlighter.gd")
var suppress_text_changed: bool = false
# Zoom functionality variables
var zoom_level: float = 1.0 # 100%
var default_font_size: int = 14 # Default font size
func _ready() -> void:
# Clear text to reset the editor state
text = ""
# YAML indentation
set_indent_size(2)
set_indent_using_spaces(true)
indent_automatic_prefixes = [":"]
scroll_smooth = true
set_highlight_current_line(true)
# Syntax highlighting
if not syntax_highlighter:
syntax_highlighter = syntax_highlighter_script.new()
# Do not lose selection when focus is lost
deselect_on_focus_loss_enabled = false
set_focus_mode(Control.FOCUS_ALL)
# Create debounce timer for content changes
snapshot_debounce_timer = Timer.new()
add_child(snapshot_debounce_timer)
snapshot_debounce_timer.one_shot = true
snapshot_debounce_timer.wait_time = 0.3 # 300ms
snapshot_debounce_timer.timeout.connect(_on_snapshot_debounce_timeout)
# Connect signals
text_changed.connect(_on_text_changed)
gui_input.connect(_on_gui_input_focus)
# Register YAML code completion
register_yaml_code_completion()
# Apply initial font size
_update_font_size()
func _on_text_changed() -> void:
if suppress_text_changed:
return
# Clear error indicators when text changes
clear_error_indicators()
# Request a snapshot with debounce
snapshot_debounce_timer.start()
func _on_snapshot_debounce_timeout() -> void:
# Emit content changed signal
content_changed.emit()
# Request validation
validation_requested.emit()
func _on_gui_input_focus(event: InputEvent) -> void:
# Grab focus when clicked
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
grab_focus()
func cut_selection() -> void:
if has_selection():
# Cut the selected text to clipboard
DisplayServer.clipboard_set(get_selected_text())
delete_selection()
else:
# If no selection, cut the current line (like default script editor)
var line := get_caret_line()
var line_text := get_line(line)
DisplayServer.clipboard_set(line_text)
# Delete the current line
select(line, 0, line, line_text.length())
delete_selection()
# If this isn't the last line, also remove the line break
if line < get_line_count() - 1:
select(line, 0, line + 1, 0)
delete_selection()
# Trigger content changed
text_changed.emit()
func copy_selection() -> void:
if has_selection():
# Copy selected text to clipboard
DisplayServer.clipboard_set(get_selected_text())
else:
# If no selection, copy the current line
var line := get_caret_line()
var line_text := get_line(line)
DisplayServer.clipboard_set(line_text)
func paste_clipboard() -> void:
# Get clipboard content
var clipboard = DisplayServer.clipboard_get()
if clipboard.is_empty():
return
if has_selection():
# Replace selected text with clipboard content
delete_selection()
# Insert clipboard content at caret position
insert_text_at_caret(clipboard)
text_changed.emit()
# Zoom management functions
func zoom_in() -> void:
zoom_level = min(zoom_level + 0.07, 3.0) # Max 200%
_update_font_size()
zoom_changed.emit(zoom_level)
func zoom_out() -> void:
zoom_level = max(zoom_level - 0.07, 0.25) # Min 50%
_update_font_size()
zoom_changed.emit(zoom_level)
func zoom_reset() -> void:
zoom_level = 1.0
_update_font_size()
zoom_changed.emit(zoom_level)
func set_zoom(zoom: float) -> void:
zoom_level = max(0.25, min(3.0, zoom))
_update_font_size()
zoom_changed.emit(zoom_level)
func _update_font_size() -> void:
var new_size = int(default_font_size * zoom_level)
add_theme_font_size_override("font_size", new_size)
func _unhandled_key_input(event: InputEvent) -> void:
# Handle tab key before focus system gets it
if event is InputEventKey and event.pressed and has_focus():
match event.keycode:
KEY_TAB:
if event.shift_pressed:
# Handle Shift+Tab for unindent
_handle_unindent()
else:
# Handle Tab for indent
_handle_indent()
get_viewport().set_input_as_handled()
return
func _gui_input(event: InputEvent) -> void:
# Handle shortcuts for saving/closing
if event is InputEventKey and event.pressed:
match event.get_keycode_with_modifiers():
KEY_MASK_CTRL | KEY_S:
save_requested.emit()
get_viewport().set_input_as_handled()
KEY_MASK_CTRL | KEY_W:
close_requested.emit()
get_viewport().set_input_as_handled()
KEY_ENTER, KEY_KP_ENTER:
# Handle auto-continuation of YAML structures
_handle_enter_key()
get_viewport().set_input_as_handled()
KEY_MASK_CTRL | KEY_Z:
# Handle undo
undo_requested.emit()
get_viewport().set_input_as_handled()
KEY_MASK_CTRL | KEY_Y, KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_Z:
# Handle redo (supports both Ctrl+Y and Ctrl+Shift+Z)
redo_requested.emit()
get_viewport().set_input_as_handled()
KEY_MASK_CTRL | KEY_EQUAL, KEY_MASK_CTRL | KEY_KP_ADD:
zoom_in()
KEY_MASK_CTRL | KEY_MINUS, KEY_MASK_CTRL | KEY_KP_SUBTRACT:
zoom_out()
KEY_MASK_CTRL | KEY_0, KEY_MASK_CTRL | KEY_KP_0:
zoom_reset()
if event is InputEventMouseButton and event.pressed and event.is_command_or_control_pressed():
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
zoom_in()
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
zoom_out()
func set_text_and_preserve_state(new_text: String, preserve_state: bool = true) -> void:
if preserve_state:
# Save current state
var previous_caret_pos := get_caret_column()
var previous_line := get_caret_line()
var previous_scroll_v := get_v_scroll_bar().value
var previous_scroll_h := get_h_scroll_bar().value
# Set text without triggering our own text_changed handler
suppress_text_changed = true
text = new_text
suppress_text_changed = false
# Restore state if possible
if previous_line < get_line_count():
set_caret_line(previous_line)
var line_length := get_line(previous_line).length()
if previous_caret_pos <= line_length:
set_caret_column(previous_caret_pos)
# Restore scroll position (with a small delay to ensure the text is updated first)
call_deferred("_restore_scroll_position", previous_scroll_v, previous_scroll_h)
else:
# Just set the text without preserving state
suppress_text_changed = true
text = new_text
suppress_text_changed = false
func _restore_scroll_position(v_scroll: float, h_scroll: float) -> void:
# Wait for one frame to ensure the text has been updated and rendered
if get_tree():
await get_tree().process_frame
get_v_scroll_bar().value = v_scroll
get_h_scroll_bar().value = h_scroll
func _handle_indent() -> void:
# Get current line and text
var line := get_caret_line()
var line_text := get_line(line)
# Get selection so we can handle multi-line indentation
var selection_active := has_selection()
var selection_from := get_selection_from_line()
var selection_to := get_selection_to_line()
if selection_active:
# Indent multiple lines
begin_complex_operation()
for i in range(selection_from, selection_to + 1):
set_line(i, " " + get_line(i))
end_complex_operation()
else:
# Simple indent - insert 2 spaces at caret position
insert_text_at_caret(" ")
# Trigger text changed to update the document
text_changed.emit()
func _handle_unindent() -> void:
# Get current line and text
var line := get_caret_line()
var text := get_line(line)
# Get selection so we can handle multi-line unindentation
var selection_active := has_selection()
var selection_from := get_selection_from_line()
var selection_to := get_selection_to_line()
if selection_active:
# Unindent multiple lines
begin_complex_operation()
for i in range(selection_from, selection_to + 1):
var line_text := get_line(i)
if line_text.begins_with(" "):
set_line(i, line_text.substr(2))
elif line_text.begins_with(" "):
set_line(i, line_text.substr(1))
end_complex_operation()
else:
# Simple unindent - remove up to 2 spaces from beginning of line
if text.begins_with(" "):
set_line(line, text.substr(2))
set_caret_column(max(0, get_caret_column() - 2))
elif text.begins_with(" "):
set_line(line, text.substr(1))
set_caret_column(max(0, get_caret_column() - 1))
func _handle_enter_key() -> void:
var line := get_caret_line()
var line_text := get_line(line)
# Auto-continuation for lists
if "- " in line_text:
var indent_level := 0
for c in line_text:
if c == ' ':
indent_level += 1
else:
break
# Insert the line with the same indentation and list marker
var new_line := "\n" + " ".repeat(indent_level) + "- "
insert_text_at_caret(new_line)
else:
# Regular line break with preserved indentation
var indent_level := 0
for c in line_text:
if c == ' ':
indent_level += 1
else:
break
# Increased indentation if the line ends with a colon
if line_text.strip_edges().ends_with(":"):
indent_level += 2
insert_text_at_caret("\n" + " ".repeat(indent_level))
func register_yaml_code_completion() -> void:
# Register common YAML keywords and patterns for code completion
var keyword_list: PackedStringArray = [
"true",
"false",
"null",
"~",
"INF",
"-INF"
]
# Add keywords and tags to completion
for keyword in keyword_list:
add_code_completion_option(CodeCompletionKind.KIND_CONSTANT, keyword, keyword)
## YAML tags
var tag_list: PackedStringArray = [
"!Resource",
"!AABB",
"!Basis",
"!Color",
"!NodePath",
"!PackedByteArray",
"!PackedColorArray",
"!PackedFloat32Array",
"!PackedFloat64Array",
"!PackedInt32Array",
"!PackedInt64Array",
"!PackedStringArray",
"!PackedVector2Array",
"!PackedVector3Array",
"!Plane",
"!Projection",
"!Quaternion",
"!Rect2",
"!Rect2i",
"!StringName",
"!Transform2D",
"!Transform3D",
"!Vector2",
"!Vector2i",
"!Vector3",
"!Vector3i",
"!Vector4",
"!Vector4i"
]
for tag in tag_list:
add_code_completion_option(CodeCompletionKind.KIND_CLASS, tag, tag)
func mark_error_line(line: int, message: String) -> void:
if line < 0 or line >= get_line_count():
return
# Set line background to error color
var error_color: Color = EditorInterface.get_editor_settings().get_setting("text_editor/theme/highlighting/mark_color")
set_line_background_color(line, error_color)
# Set gutter icon
var error_icon := get_theme_icon("StatusError", "EditorIcons")
if error_icon:
set_line_gutter_icon(line, 0, error_icon)
# Store for later reference
error_indicators[line] = message
func clear_error_indicators() -> void:
for line: int in error_indicators:
set_line_background_color(line, Color(0, 0, 0, 0))
set_line_gutter_icon(line, 0, null)
error_indicators.clear()
func get_current_line_col_info() -> Array[int]:
var line := get_caret_line() + 1
var col := get_caret_column() + 1
return [line, col]