Add Recipemanager that loads recipes from YAML
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
class_name YAMLCodeEdit extends CodeEdit
|
||||
|
||||
var syntax_highlighter_script = preload("res://addons/yaml/editor/syntax_highlighting/syntax_highlighter.gd")
|
||||
|
||||
func _init() -> void:
|
||||
# YAML indentation
|
||||
set_indent_size(2)
|
||||
set_indent_using_spaces(true)
|
||||
indent_automatic = true
|
||||
indent_automatic_prefixes = [":"]
|
||||
|
||||
# Syntax highlighting
|
||||
if not syntax_highlighter:
|
||||
syntax_highlighter = syntax_highlighter_script.new()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bex4rlxea675g
|
||||
@@ -0,0 +1,394 @@
|
||||
@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]
|
||||
@@ -0,0 +1 @@
|
||||
uid://drkui6da5o1ou
|
||||
@@ -0,0 +1,136 @@
|
||||
@tool
|
||||
class_name YAMLEditorDocument extends RefCounted
|
||||
|
||||
# Primary document data
|
||||
var path: String
|
||||
var content: String
|
||||
var is_modified: bool = false
|
||||
var validation_result: YAMLResult
|
||||
|
||||
# History management
|
||||
class YAMLEditorHistoryState extends RefCounted:
|
||||
var text: String
|
||||
var caret_line: int = 0
|
||||
var caret_column: int = 0
|
||||
|
||||
func _init(p_text: String, p_line: int = 0, p_column: int = 0) -> void:
|
||||
text = p_text
|
||||
caret_line = p_line
|
||||
caret_column = p_column
|
||||
|
||||
func _to_string() -> String:
|
||||
return "YAMLEditorHistoryState(text_length=%d, line=%d, column=%d)" % [text.length(), caret_line, caret_column]
|
||||
|
||||
# Limit history size to prevent excessive memory use
|
||||
const MAX_HISTORY := 100
|
||||
|
||||
var history_states: Array[YAMLEditorHistoryState] = []
|
||||
var current_history_index: int = -1
|
||||
var saved_history_index: int = -1
|
||||
|
||||
# Signals
|
||||
signal content_changed(document)
|
||||
signal validation_changed(document)
|
||||
signal modified_changed(document)
|
||||
|
||||
# Constructor
|
||||
func _init(p_path: String, p_content: String = "") -> void:
|
||||
path = p_path
|
||||
content = p_content
|
||||
validation_result = YAMLResult.new() # Empty result
|
||||
|
||||
# Take initial snapshot if content isn't empty
|
||||
if not p_content.is_empty():
|
||||
_add_history_state(YAMLEditorHistoryState.new(p_content))
|
||||
|
||||
# File path utilities
|
||||
func get_file_name() -> String:
|
||||
return path.get_file()
|
||||
|
||||
func is_untitled() -> bool:
|
||||
return path.begins_with("untitled")
|
||||
|
||||
# Content management
|
||||
func set_content(new_content: String, caret_line: int = 0, caret_column: int = 0) -> void:
|
||||
if content == new_content:
|
||||
return
|
||||
|
||||
content = new_content
|
||||
_add_history_state(YAMLEditorHistoryState.new(new_content, caret_line, caret_column))
|
||||
set_modified(true)
|
||||
content_changed.emit(self)
|
||||
|
||||
# Modification state
|
||||
func set_modified(modified: bool) -> void:
|
||||
if is_modified == modified:
|
||||
return
|
||||
|
||||
is_modified = modified
|
||||
modified_changed.emit(self)
|
||||
|
||||
# Validation management
|
||||
func set_validation_result(result: YAMLResult) -> void:
|
||||
validation_result = result
|
||||
validation_changed.emit(self)
|
||||
|
||||
func has_error() -> bool:
|
||||
return validation_result and validation_result.has_error()
|
||||
|
||||
# History management
|
||||
func can_undo() -> bool:
|
||||
return current_history_index > 0 # Need at least one previous state
|
||||
|
||||
func can_redo() -> bool:
|
||||
return current_history_index < history_states.size() - 1
|
||||
|
||||
func undo() -> YAMLEditorHistoryState:
|
||||
if not can_undo():
|
||||
return null
|
||||
|
||||
current_history_index -= 1
|
||||
var state := history_states[current_history_index]
|
||||
content = state.text
|
||||
|
||||
# Update modification state
|
||||
set_modified(current_history_index != saved_history_index)
|
||||
content_changed.emit(self)
|
||||
|
||||
return state
|
||||
|
||||
func redo() -> YAMLEditorHistoryState:
|
||||
if not can_redo():
|
||||
return null
|
||||
|
||||
current_history_index += 1
|
||||
var state := history_states[current_history_index]
|
||||
content = state.text
|
||||
|
||||
# Update modification state
|
||||
set_modified(current_history_index != saved_history_index)
|
||||
content_changed.emit(self)
|
||||
|
||||
return state
|
||||
|
||||
func mark_saved() -> void:
|
||||
saved_history_index = current_history_index
|
||||
set_modified(false)
|
||||
|
||||
func _add_history_state(state: YAMLEditorHistoryState) -> void:
|
||||
# If we're not at the end of history, truncate future states
|
||||
if current_history_index < history_states.size() - 1:
|
||||
history_states = history_states.slice(0, current_history_index + 1)
|
||||
|
||||
# Add the new state
|
||||
history_states.append(state)
|
||||
current_history_index = history_states.size() - 1
|
||||
|
||||
if history_states.size() > MAX_HISTORY:
|
||||
var excess := history_states.size() - MAX_HISTORY
|
||||
history_states = history_states.slice(excess)
|
||||
current_history_index -= excess
|
||||
|
||||
# Adjust saved index if needed
|
||||
if saved_history_index >= 0:
|
||||
saved_history_index -= excess
|
||||
if saved_history_index < 0:
|
||||
saved_history_index = -1
|
||||
@@ -0,0 +1 @@
|
||||
uid://5ow36wsuc7uj
|
||||
@@ -0,0 +1,578 @@
|
||||
@tool
|
||||
class_name YAMLEditorDocumentManager extends Node
|
||||
|
||||
signal document_changed(document)
|
||||
signal document_created(document)
|
||||
signal document_closed(document)
|
||||
|
||||
# Dictionary of open documents: {path: YAMLEditorDocument}
|
||||
var documents: Dictionary = {}
|
||||
var current_document: YAMLEditorDocument = null
|
||||
|
||||
# UI components
|
||||
var file_list: YAMLEditorFileList
|
||||
var code_editor: YAMLCodeEditor
|
||||
var file_popup_menu: PopupMenu
|
||||
var editor_node: Control
|
||||
|
||||
# Reference to the singleton
|
||||
var file_system: YAMLFileSystem
|
||||
|
||||
# Track recently saved files to avoid external update conflicts
|
||||
var recently_saved_files: Dictionary = {}
|
||||
var ignore_update_timer: Timer
|
||||
|
||||
func _init(_editor: Control) -> void:
|
||||
editor_node = _editor
|
||||
|
||||
func _ready() -> void:
|
||||
# Get singleton reference
|
||||
file_system = YAMLFileSystem.get_singleton()
|
||||
|
||||
# Listen for external file updates
|
||||
file_system.file_updated.connect(_on_external_file_updated)
|
||||
file_system.file_renamed.connect(_on_file_renamed)
|
||||
|
||||
# Create file popup menu
|
||||
file_popup_menu = PopupMenu.new()
|
||||
add_child(file_popup_menu)
|
||||
|
||||
# Add menu items
|
||||
file_popup_menu.add_item("Save", 0)
|
||||
file_popup_menu.add_item("Save As...", 1)
|
||||
file_popup_menu.add_separator()
|
||||
file_popup_menu.add_item("Close", 2)
|
||||
file_popup_menu.add_separator()
|
||||
file_popup_menu.add_item("Show in FileSystem", 3)
|
||||
|
||||
# Connect popup menu signals
|
||||
file_popup_menu.id_pressed.connect(_on_file_popup_menu_id_pressed)
|
||||
|
||||
# Create timer for clearing recent saves
|
||||
ignore_update_timer = Timer.new()
|
||||
add_child(ignore_update_timer)
|
||||
ignore_update_timer.one_shot = true
|
||||
ignore_update_timer.wait_time = 0.5 # 500ms
|
||||
ignore_update_timer.timeout.connect(_on_ignore_update_timer_timeout)
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
func setup(p_file_list: YAMLEditorFileList, p_code_editor: YAMLCodeEditor) -> void:
|
||||
file_list = p_file_list
|
||||
code_editor = p_code_editor
|
||||
|
||||
# Connect signals from file list component
|
||||
file_list.file_selected.connect(_on_file_selected)
|
||||
file_list.file_context_requested.connect(_on_file_context_requested)
|
||||
|
||||
func create_document(path: String, content: String = "") -> YAMLEditorDocument:
|
||||
var normalized_path = _normalize_path(path)
|
||||
|
||||
# Check if document already exists with normalized path
|
||||
if documents.has(normalized_path):
|
||||
return documents[normalized_path]
|
||||
|
||||
var document := YAMLEditorDocument.new(normalized_path, content)
|
||||
|
||||
# Connect document signals
|
||||
document.content_changed.connect(_on_document_content_changed)
|
||||
document.modified_changed.connect(_on_document_modified_changed)
|
||||
|
||||
# Store document
|
||||
documents[normalized_path] = document
|
||||
document_created.emit(document)
|
||||
|
||||
return document
|
||||
|
||||
func open_file(path: String) -> void:
|
||||
var normalized_path = _normalize_path(path)
|
||||
|
||||
# Check if already open
|
||||
if documents.has(normalized_path):
|
||||
set_current_document(documents[normalized_path])
|
||||
return
|
||||
|
||||
# Look for any document with the same base filename
|
||||
var filename = normalized_path.get_file()
|
||||
for existing_path in documents.keys():
|
||||
if existing_path.get_file() == filename and existing_path != normalized_path:
|
||||
# Check if they point to the same actual file
|
||||
if _paths_point_to_same_file(normalized_path, existing_path):
|
||||
set_current_document(documents[existing_path])
|
||||
return
|
||||
|
||||
# Use the file system singleton to read the file
|
||||
var content := file_system.read_file(normalized_path)
|
||||
if typeof(content) == TYPE_INT: # Error code
|
||||
push_error("Could not open file '%s': %s" % [normalized_path, error_string(content)])
|
||||
return
|
||||
|
||||
# Create new document
|
||||
var document := create_document(normalized_path, content)
|
||||
document.mark_saved() # Initial state is saved
|
||||
|
||||
# Switch to the new document
|
||||
set_current_document(document)
|
||||
|
||||
# Notify the file system
|
||||
file_system.notify_file_opened(normalized_path)
|
||||
|
||||
func close_document(document: YAMLEditorDocument) -> bool:
|
||||
if document == null:
|
||||
return true
|
||||
|
||||
if document.is_modified:
|
||||
# Show confirmation dialog for unsaved changes
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.title = "Unsaved Changes"
|
||||
dialog.dialog_text = "Save changes to '" + document.get_file_name() + "' before closing?"
|
||||
dialog.add_button("Don't Save", true, "dont_save")
|
||||
dialog.add_cancel_button("Cancel")
|
||||
|
||||
dialog.confirmed.connect(
|
||||
func():
|
||||
# Save was chosen
|
||||
if save_document(document):
|
||||
_close_document_internal(document)
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
dialog.custom_action.connect(
|
||||
func(action):
|
||||
if action == "dont_save":
|
||||
_close_document_internal(document)
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
dialog.canceled.connect(func(): dialog.queue_free())
|
||||
|
||||
add_child(dialog)
|
||||
dialog.popup_centered()
|
||||
return false
|
||||
|
||||
return _close_document_internal(document)
|
||||
|
||||
func _close_document_internal(document: YAMLEditorDocument) -> bool:
|
||||
if document == null:
|
||||
return false
|
||||
|
||||
# Find the document in our dictionary
|
||||
var path_to_remove = ""
|
||||
for path in documents.keys():
|
||||
if documents[path] == document:
|
||||
path_to_remove = path
|
||||
break
|
||||
|
||||
if path_to_remove.is_empty():
|
||||
return false
|
||||
|
||||
# Notify document is being closed
|
||||
document_closed.emit(document)
|
||||
|
||||
# Remove document
|
||||
documents.erase(document.path)
|
||||
|
||||
# If this was the current document, switch to another
|
||||
if current_document == document:
|
||||
current_document = null
|
||||
|
||||
# Select another document if available
|
||||
if not documents.is_empty():
|
||||
set_current_document(documents.values()[0])
|
||||
else:
|
||||
# Clear the editor if no documents left
|
||||
code_editor.text = ""
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
# Notify the file system
|
||||
file_system.notify_file_closed(document.path)
|
||||
|
||||
return true
|
||||
|
||||
func save_document(document: YAMLEditorDocument) -> bool:
|
||||
if document == null:
|
||||
return false
|
||||
|
||||
# Don't save untitled files directly
|
||||
if document.is_untitled():
|
||||
return false # Caller should handle "Save As" dialog
|
||||
|
||||
# Mark this file as recently saved to ignore update notifications
|
||||
recently_saved_files[document.path] = Time.get_unix_time_from_system()
|
||||
ignore_update_timer.start()
|
||||
|
||||
# Use the file system singleton to save the file
|
||||
var result := file_system.save_file(document.path, document.content)
|
||||
if result != OK:
|
||||
push_error("Could not save file '%s': %s" % [document.path, error_string(result)])
|
||||
recently_saved_files.erase(document.path) # Remove from recently saved if error
|
||||
return false
|
||||
|
||||
# Mark document as saved
|
||||
document.mark_saved()
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
return true
|
||||
|
||||
func save_document_as(document: YAMLEditorDocument, new_path: String) -> bool:
|
||||
if document == null or new_path.is_empty():
|
||||
return false
|
||||
|
||||
var normalized_new_path = _normalize_path(new_path)
|
||||
|
||||
# Check if we're trying to save to a path that's already open
|
||||
if documents.has(normalized_new_path) and documents[normalized_new_path] != document:
|
||||
push_error("Cannot save as '%s' - file is already open" % normalized_new_path)
|
||||
return false
|
||||
|
||||
# Remember the old path
|
||||
var old_path := document.path
|
||||
|
||||
# Update document path
|
||||
document.path = normalized_new_path
|
||||
|
||||
# Update the documents dictionary
|
||||
if old_path != normalized_new_path:
|
||||
documents.erase(old_path)
|
||||
documents[normalized_new_path] = document
|
||||
|
||||
# Save the document
|
||||
if save_document(document):
|
||||
# If old path was temporary, clean up
|
||||
if old_path != normalized_new_path and old_path.begins_with("untitled"):
|
||||
file_system.notify_file_closed(old_path)
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
# Notify file system
|
||||
file_system.notify_file_opened(new_path)
|
||||
|
||||
return true
|
||||
|
||||
# Restore old path if save failed
|
||||
if old_path != normalized_new_path:
|
||||
document.path = old_path
|
||||
documents.erase(normalized_new_path)
|
||||
documents[old_path] = document
|
||||
|
||||
return false
|
||||
|
||||
func new_file() -> void:
|
||||
# Create a new untitled file
|
||||
var untitled_name := "untitled.yaml"
|
||||
var index := 1
|
||||
|
||||
while documents.has(untitled_name):
|
||||
index += 1
|
||||
untitled_name = "untitled%d.yaml" % index
|
||||
|
||||
# Create a new document
|
||||
var document := create_document(untitled_name)
|
||||
document.set_modified(true) # New document is always modified
|
||||
|
||||
# Switch to the new document
|
||||
set_current_document(document)
|
||||
|
||||
# Set focus to code editor
|
||||
code_editor.grab_focus()
|
||||
|
||||
# Notify the file system
|
||||
file_system.notify_file_opened(untitled_name)
|
||||
|
||||
func set_current_document(document: YAMLEditorDocument) -> void:
|
||||
if document == null or document == current_document:
|
||||
return
|
||||
|
||||
current_document = document
|
||||
|
||||
# Update editor content
|
||||
if is_instance_valid(code_editor):
|
||||
code_editor.set_text_and_preserve_state(document.content)
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
# Emit signal
|
||||
document_changed.emit(document)
|
||||
|
||||
func update_document_content(document: YAMLEditorDocument, new_content: String) -> void:
|
||||
if document == null:
|
||||
return
|
||||
|
||||
var caret_line := 0
|
||||
var caret_column := 0
|
||||
|
||||
if is_instance_valid(code_editor):
|
||||
caret_line = code_editor.get_caret_line()
|
||||
caret_column = code_editor.get_caret_column()
|
||||
|
||||
document.set_content(new_content, caret_line, caret_column)
|
||||
|
||||
func _normalize_path(path: String) -> String:
|
||||
# Convert to absolute path and normalize
|
||||
var normalized = path
|
||||
|
||||
# Handle different path formats
|
||||
if normalized.begins_with("res://"):
|
||||
normalized = ProjectSettings.globalize_path(normalized)
|
||||
|
||||
# Convert to canonical form
|
||||
normalized = normalized.simplify_path()
|
||||
|
||||
# Convert back to res:// format if it was originally a project path
|
||||
if path.begins_with("res://"):
|
||||
normalized = ProjectSettings.localize_path(normalized)
|
||||
|
||||
return normalized
|
||||
|
||||
func _paths_point_to_same_file(path1: String, path2: String) -> bool:
|
||||
# For project files, compare the res:// paths
|
||||
if path1.begins_with("res://") and path2.begins_with("res://"):
|
||||
return path1 == path2
|
||||
|
||||
# For absolute paths, normalize and compare
|
||||
var abs_path1 = ProjectSettings.globalize_path(path1) if path1.begins_with("res://") else path1
|
||||
var abs_path2 = ProjectSettings.globalize_path(path2) if path2.begins_with("res://") else path2
|
||||
|
||||
return abs_path1.simplify_path() == abs_path2.simplify_path()
|
||||
|
||||
func _on_document_content_changed(document: YAMLEditorDocument) -> void:
|
||||
# Update UI if this is the current document
|
||||
if document == current_document:
|
||||
update_ui()
|
||||
|
||||
func _on_document_modified_changed(document: YAMLEditorDocument) -> void:
|
||||
# Update UI if this is the current document
|
||||
if document == current_document:
|
||||
update_ui()
|
||||
|
||||
func update_ui() -> void:
|
||||
# Toggle editor visibility
|
||||
editor_node.visible = documents.size() > 0
|
||||
|
||||
if not is_instance_valid(file_list):
|
||||
return
|
||||
|
||||
# Prepare file data for the file list component
|
||||
var file_data := {}
|
||||
for path in documents:
|
||||
var document = documents[path]
|
||||
file_data[path] = {
|
||||
"name": document.get_file_name(),
|
||||
"modified": document.is_modified
|
||||
}
|
||||
|
||||
# Update the file list component
|
||||
var current_path = current_document.path if current_document else ""
|
||||
file_list.update_files(file_data, current_path)
|
||||
|
||||
func _on_file_selected(path: String) -> void:
|
||||
if path.is_empty():
|
||||
return
|
||||
|
||||
var normalized_path = _normalize_path(path)
|
||||
if not documents.has(normalized_path):
|
||||
return
|
||||
|
||||
set_current_document(documents[normalized_path])
|
||||
|
||||
func _on_file_context_requested(path: String, at_position: Vector2) -> void:
|
||||
if path.is_empty():
|
||||
return
|
||||
|
||||
var normalized_path = _normalize_path(path)
|
||||
if not documents.has(normalized_path):
|
||||
return
|
||||
|
||||
set_current_document(documents[normalized_path])
|
||||
|
||||
# Calculate the global position for the popup
|
||||
var global_rect := Rect2(file_list.get_global_mouse_position(), Vector2.ZERO)
|
||||
file_popup_menu.popup_on_parent(global_rect)
|
||||
|
||||
func _on_file_popup_menu_id_pressed(id: int) -> void:
|
||||
var path := file_list.get_selected_file_path()
|
||||
if path.is_empty():
|
||||
return
|
||||
|
||||
var normalized_path = _normalize_path(path)
|
||||
if not documents.has(normalized_path):
|
||||
return
|
||||
|
||||
var document: YAMLEditorDocument = documents[normalized_path]
|
||||
|
||||
match id:
|
||||
0: # Save
|
||||
save_document(document)
|
||||
1: # Save As
|
||||
# Main editor should handle the save as dialog
|
||||
set_current_document(document)
|
||||
2: # Close
|
||||
close_document(document)
|
||||
3: # Show in FileSystem
|
||||
if not document.is_untitled() and document.path.begins_with("res://"):
|
||||
EditorInterface.get_file_system_dock().navigate_to_path(document.path)
|
||||
|
||||
func _on_external_file_updated(path: String) -> void:
|
||||
var normalized_path = _normalize_path(path)
|
||||
|
||||
# Only process if the file is open and it's a YAML file
|
||||
if documents.has(normalized_path) and file_system.is_yaml_file(normalized_path):
|
||||
# Check if we just saved this file ourselves
|
||||
if recently_saved_files.has(normalized_path):
|
||||
var save_time: int = recently_saved_files[normalized_path]
|
||||
var current_time := Time.get_unix_time_from_system()
|
||||
|
||||
# If saved less than 1 second ago, ignore this update
|
||||
if current_time - save_time < 1.0:
|
||||
return
|
||||
|
||||
var document: YAMLEditorDocument = documents[normalized_path]
|
||||
|
||||
# Check if the document has unsaved changes
|
||||
if not document.is_modified:
|
||||
# Document is not modified locally, safe to reload
|
||||
var content = file_system.read_file(normalized_path)
|
||||
if typeof(content) != TYPE_INT: # Not an error
|
||||
# Update document content
|
||||
document.content = content
|
||||
document.mark_saved()
|
||||
|
||||
# If this is the current document, update the editor
|
||||
if document == current_document:
|
||||
# Preserve cursor position and scroll state
|
||||
var previous_caret_line := code_editor.get_caret_line()
|
||||
var previous_caret_column := code_editor.get_caret_column()
|
||||
var previous_scroll_v := code_editor.get_v_scroll_bar().value
|
||||
var previous_scroll_h := code_editor.get_h_scroll_bar().value
|
||||
|
||||
code_editor.text = content
|
||||
|
||||
# Restore position if possible
|
||||
if previous_caret_line < code_editor.get_line_count():
|
||||
code_editor.set_caret_line(previous_caret_line)
|
||||
var line_length := code_editor.get_line(previous_caret_line).length()
|
||||
if previous_caret_column <= line_length:
|
||||
code_editor.set_caret_column(previous_caret_column)
|
||||
|
||||
# Restore scroll position
|
||||
code_editor.get_v_scroll_bar().value = previous_scroll_v
|
||||
code_editor.get_h_scroll_bar().value = previous_scroll_h
|
||||
|
||||
update_ui()
|
||||
else:
|
||||
# Document has unsaved changes, show conflict dialog
|
||||
if document == current_document:
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.title = "External Changes Detected"
|
||||
dialog.dialog_text = "The file '" + document.get_file_name() + "' has been modified externally. Do you want to reload it and lose your changes?"
|
||||
dialog.confirmed.connect(
|
||||
func():
|
||||
var content := file_system.read_file(path)
|
||||
if typeof(content) != TYPE_INT:
|
||||
document.content = content
|
||||
document.mark_saved()
|
||||
|
||||
if document == current_document:
|
||||
code_editor.text = content
|
||||
|
||||
update_ui()
|
||||
dialog.queue_free()
|
||||
)
|
||||
dialog.canceled.connect(func(): dialog.queue_free())
|
||||
add_child(dialog)
|
||||
dialog.popup_centered()
|
||||
|
||||
func _on_ignore_update_timer_timeout() -> void:
|
||||
# Clear out any old saved entries
|
||||
var current_time := Time.get_unix_time_from_system()
|
||||
var keys_to_remove: PackedStringArray = []
|
||||
|
||||
for path in recently_saved_files:
|
||||
var save_time = recently_saved_files[path]
|
||||
if current_time - save_time >= 1.0:
|
||||
keys_to_remove.append(path)
|
||||
|
||||
for path in keys_to_remove:
|
||||
recently_saved_files.erase(path)
|
||||
|
||||
func _on_file_renamed(old_path: String, new_path: String) -> void:
|
||||
var normalized_old_path = _normalize_path(old_path)
|
||||
var normalized_new_path = _normalize_path(new_path)
|
||||
|
||||
# If we have this document open, update our references
|
||||
if documents.has(normalized_old_path):
|
||||
var document: YAMLEditorDocument = documents[normalized_old_path]
|
||||
document.path = normalized_new_path
|
||||
|
||||
documents.erase(normalized_old_path)
|
||||
documents[normalized_new_path] = document
|
||||
|
||||
update_ui()
|
||||
|
||||
func handle_filesystem_change() -> void:
|
||||
# Check if any of our open res:// files no longer exist
|
||||
var missing_files: PackedStringArray = []
|
||||
|
||||
for path in documents.keys():
|
||||
if path.begins_with("res://") and not file_system.file_exists(path):
|
||||
missing_files.append(path)
|
||||
|
||||
# Handle missing files
|
||||
for old_path in missing_files:
|
||||
var document: YAMLEditorDocument = documents[old_path]
|
||||
|
||||
# Try to find a file with the same name but different path in the filesystem
|
||||
var filename := old_path.get_file()
|
||||
var filesystem_root := EditorInterface.get_resource_filesystem().get_filesystem()
|
||||
var new_path := file_system.find_file_in_filesystem(filesystem_root, filename)
|
||||
|
||||
if not new_path.is_empty():
|
||||
var normalized_new_path = _normalize_path(new_path)
|
||||
|
||||
# Found potential match - update the document path
|
||||
document.path = normalized_new_path
|
||||
documents.erase(old_path)
|
||||
documents[normalized_new_path] = document
|
||||
|
||||
# If this is the current document, emit signal
|
||||
if document == current_document:
|
||||
document_changed.emit(document)
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
# Notify file system
|
||||
file_system.notify_file_closed(old_path)
|
||||
file_system.notify_file_opened(normalized_new_path)
|
||||
file_system.notify_file_renamed(old_path, normalized_new_path)
|
||||
else:
|
||||
# Keep it open but mark as potentially moved/deleted to avoid losing unsaved changes
|
||||
pass
|
||||
|
||||
func has_unsaved_changes() -> bool:
|
||||
for document in documents.values():
|
||||
if document.is_modified:
|
||||
return true
|
||||
return false
|
||||
|
||||
func get_open_documents() -> Array:
|
||||
return documents.values()
|
||||
|
||||
func get_open_paths() -> Array:
|
||||
return documents.keys()
|
||||
|
||||
func has_document(path: String) -> bool:
|
||||
return documents.has(path)
|
||||
|
||||
func get_document(path: String) -> YAMLEditorDocument:
|
||||
return documents.get(path, null)
|
||||
|
||||
func get_current_document() -> YAMLEditorDocument:
|
||||
return current_document
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpm2siqxhgj8w
|
||||
@@ -0,0 +1,166 @@
|
||||
@tool
|
||||
class_name YAMLEditorShortcuts
|
||||
|
||||
# This helper class registers editor shortcuts for the YAML editor
|
||||
|
||||
const SHORTCUTS = [
|
||||
{
|
||||
"name": "Save",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_S,
|
||||
"callback": "_on_save_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Save As",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_S,
|
||||
"callback": "_on_save_as_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Close File",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_W,
|
||||
"callback": "_on_close_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "New File",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_N,
|
||||
"callback": "_on_new_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Open File",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_O,
|
||||
"callback": "_on_open_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Validate YAML",
|
||||
"shortcut": KEY_F4,
|
||||
"callback": "_on_validate_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Undo",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_Z,
|
||||
"callback": "_on_undo_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Redo",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_Z,
|
||||
"callback": "_on_redo_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Cut",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_X,
|
||||
"callback": "_on_cut_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Copy",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_C,
|
||||
"callback": "_on_copy_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Paste",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_V,
|
||||
"callback": "_on_paste_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Select All",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_A,
|
||||
"callback": "_on_select_all_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Find",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_F,
|
||||
"callback": "_on_find_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Find Next",
|
||||
"shortcut": KEY_F3,
|
||||
"callback": "_on_find_next_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Find Previous",
|
||||
"shortcut": KEY_MASK_SHIFT | KEY_F3,
|
||||
"callback": "_on_find_previous_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Replace",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_R,
|
||||
"callback": "_on_replace_shortcut"
|
||||
},
|
||||
# Zoom shortcuts
|
||||
{
|
||||
"name": "Zoom In",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_EQUAL,
|
||||
"callback": "_on_zoom_in_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Zoom In (Numpad)",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_KP_ADD,
|
||||
"callback": "_on_zoom_in_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Zoom Out",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_MINUS,
|
||||
"callback": "_on_zoom_out_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Zoom Out (Numpad)",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_KP_SUBTRACT,
|
||||
"callback": "_on_zoom_out_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Reset Zoom",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_0,
|
||||
"callback": "_on_zoom_reset_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Reset Zoom (Numpad)",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_KP_0,
|
||||
"callback": "_on_zoom_reset_shortcut"
|
||||
}
|
||||
]
|
||||
|
||||
static func register_shortcuts(editor_plugin: EditorPlugin, target_object: Object) -> void:
|
||||
# Create shortcut inputs for the YAML editor
|
||||
var editor_settings := editor_plugin.get_editor_interface().get_editor_settings()
|
||||
var shortcuts_settings := editor_settings.get_setting("shortcuts") if editor_settings.has_setting("shortcuts") else {}
|
||||
|
||||
# Create a unique editor name for our shortcuts
|
||||
var editor_name := "YAML Editor"
|
||||
|
||||
# Register each shortcut
|
||||
for shortcut_data in SHORTCUTS:
|
||||
var shortcut_name: String = "yaml_editor/" + shortcut_data.name.to_lower().replace(" ", "_")
|
||||
var input_event := InputEventKey.new()
|
||||
input_event.keycode = shortcut_data.shortcut
|
||||
|
||||
# Create a shortcut
|
||||
var shortcut := Shortcut.new()
|
||||
shortcut.events = [input_event]
|
||||
|
||||
# Register the shortcut with Godot's input map
|
||||
if not InputMap.has_action(shortcut_name):
|
||||
InputMap.add_action(shortcut_name)
|
||||
InputMap.action_add_event(shortcut_name, input_event)
|
||||
|
||||
# Connect to the target object's _unhandled_key_input method if it exists
|
||||
if !target_object.has_method("_unhandled_key_input"):
|
||||
# Create connections for shortcuts if the target doesn't handle key input directly
|
||||
for shortcut_data in SHORTCUTS:
|
||||
var shortcut_name: String = "yaml_editor/" + shortcut_data.name.to_lower().replace(" ", "_")
|
||||
if target_object.has_method(shortcut_data.callback):
|
||||
InputMap.action_add_event(shortcut_name, InputEventAction.new())
|
||||
# Connect the shortcut action to the target's callback
|
||||
var root := editor_plugin.get_tree().root
|
||||
root.connect("input_event",
|
||||
func(event):
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.get_keycode_with_modifiers() == shortcut_data.shortcut:
|
||||
target_object.call(shortcut_data.callback)
|
||||
print("called a thing")
|
||||
root.get_viewport().set_input_as_handled()
|
||||
)
|
||||
|
||||
static func unregister_shortcuts() -> void:
|
||||
# Remove all registered shortcuts
|
||||
for shortcut_data in SHORTCUTS:
|
||||
var shortcut_name: String = "yaml_editor/" + shortcut_data.name.to_lower().replace(" ", "_")
|
||||
if InputMap.has_action(shortcut_name):
|
||||
InputMap.erase_action(shortcut_name)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dgcufn1xonjkp
|
||||
@@ -0,0 +1,107 @@
|
||||
@tool
|
||||
class_name YAMLEditorFileList extends VBoxContainer
|
||||
|
||||
signal file_selected(path)
|
||||
signal file_context_requested(path, position)
|
||||
|
||||
# References to UI components
|
||||
@export var filter_input: LineEdit
|
||||
@export var file_list: ItemList
|
||||
|
||||
# File data
|
||||
var files: Dictionary = {} # {path: {name, modified}}
|
||||
var filtered_files: Array = []
|
||||
var current_path: String = ""
|
||||
|
||||
func _ready() -> void:
|
||||
# SessionManager will handle loading of the files
|
||||
file_list.clear()
|
||||
|
||||
# Connect internal signals
|
||||
if is_instance_valid(file_list):
|
||||
file_list.item_selected.connect(_on_item_selected)
|
||||
file_list.item_clicked.connect(_on_item_clicked)
|
||||
|
||||
if is_instance_valid(filter_input):
|
||||
filter_input.text_changed.connect(_on_filter_text_changed)
|
||||
filter_input.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||
|
||||
# Public API
|
||||
func update_files(p_files: Dictionary, p_current_path: String) -> void:
|
||||
files = p_files.duplicate()
|
||||
current_path = p_current_path
|
||||
_update_ui()
|
||||
|
||||
func mark_file_modified(path: String, is_modified: bool) -> void:
|
||||
if files.has(path):
|
||||
files[path].modified = is_modified
|
||||
_update_ui()
|
||||
|
||||
func get_selected_file_path() -> String:
|
||||
if not is_instance_valid(file_list):
|
||||
return ""
|
||||
|
||||
var selected_items := file_list.get_selected_items()
|
||||
if selected_items.is_empty():
|
||||
return ""
|
||||
|
||||
var selected_index := selected_items[0]
|
||||
if selected_index >= 0 and selected_index < filtered_files.size():
|
||||
return filtered_files[selected_index]
|
||||
|
||||
return ""
|
||||
|
||||
# UI update
|
||||
func _update_ui() -> void:
|
||||
if not is_instance_valid(file_list):
|
||||
return
|
||||
|
||||
var current_selection := get_selected_file_path()
|
||||
|
||||
file_list.clear()
|
||||
filtered_files.clear()
|
||||
|
||||
var filter_text := filter_input.text.to_lower() if is_instance_valid(filter_input) else ""
|
||||
|
||||
var current_index := -1
|
||||
var index := 0
|
||||
|
||||
for path: String in files.keys():
|
||||
var file_data: Dictionary = files[path]
|
||||
var file_name := path.get_file()
|
||||
|
||||
if not filter_text.is_empty() and file_name.to_lower().find(filter_text) == -1:
|
||||
continue
|
||||
|
||||
var display_name := file_name
|
||||
if file_data.modified:
|
||||
display_name += " (*)"
|
||||
|
||||
file_list.add_item(display_name)
|
||||
filtered_files.append(path)
|
||||
file_list.set_item_tooltip(index, path)
|
||||
|
||||
if path == current_path:
|
||||
current_index = index
|
||||
|
||||
if path == current_selection:
|
||||
file_list.select(index)
|
||||
|
||||
index += 1
|
||||
|
||||
if current_index >= 0 and (file_list.get_selected_items().is_empty() or current_path != current_selection):
|
||||
file_list.select(current_index)
|
||||
|
||||
# Signal handlers
|
||||
func _on_filter_text_changed(_text: String) -> void:
|
||||
_update_ui()
|
||||
|
||||
func _on_item_selected(index: int) -> void:
|
||||
if index >= 0 and index < filtered_files.size():
|
||||
file_selected.emit(filtered_files[index])
|
||||
|
||||
func _on_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
|
||||
if index >= 0 and index < filtered_files.size():
|
||||
if mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||
file_list.select(index)
|
||||
file_context_requested.emit(filtered_files[index], at_position)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c15odacm31d03
|
||||
@@ -0,0 +1,93 @@
|
||||
@tool
|
||||
class_name YAMLFileSystem extends Node
|
||||
|
||||
signal file_opened(path)
|
||||
signal file_saved(path)
|
||||
signal file_updated(path)
|
||||
signal file_closed(path)
|
||||
signal file_renamed(old_path, new_path)
|
||||
|
||||
# Singleton pattern
|
||||
static var _instance: YAMLFileSystem
|
||||
static func get_singleton() -> YAMLFileSystem:
|
||||
if not _instance:
|
||||
_instance = YAMLFileSystem.new()
|
||||
Engine.get_main_loop().root.call_deferred("add_child", _instance)
|
||||
return _instance
|
||||
|
||||
func _init() -> void:
|
||||
if _instance != null:
|
||||
push_error("YAMLFileSystem singleton already exists")
|
||||
return
|
||||
_instance = self
|
||||
# Mark as persistent so it doesn't get destroyed on scene changes
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
||||
# File operations with signals
|
||||
func save_file(path: String, content: String) -> Error:
|
||||
var was_new_file = not file_exists(path)
|
||||
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if not file:
|
||||
return FileAccess.get_open_error()
|
||||
|
||||
file.store_string(content)
|
||||
file_saved.emit(path)
|
||||
file_updated.emit(path)
|
||||
|
||||
# If this was a new file, notify Godot's filesystem
|
||||
if was_new_file:
|
||||
call_deferred("_refresh_filesystem", path)
|
||||
|
||||
return OK
|
||||
|
||||
func read_file(path: String) -> Variant:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if not file:
|
||||
return FileAccess.get_open_error()
|
||||
|
||||
return file.get_as_text()
|
||||
|
||||
# Check if a file exists
|
||||
func file_exists(path: String) -> bool:
|
||||
return FileAccess.file_exists(path)
|
||||
|
||||
# Utility to check if a path is a YAML file
|
||||
func is_yaml_file(path: String) -> bool:
|
||||
return path.get_extension().to_lower() in ["yaml", "yml"]
|
||||
|
||||
# For external updates, allow code to manually trigger the signal
|
||||
func notify_file_updated(path: String) -> void:
|
||||
file_updated.emit(path)
|
||||
|
||||
# Called when a file is opened in the editor
|
||||
func notify_file_opened(path: String) -> void:
|
||||
file_opened.emit(path)
|
||||
|
||||
# Called when a file is closed in the editor
|
||||
func notify_file_closed(path: String) -> void:
|
||||
file_closed.emit(path)
|
||||
|
||||
# Called when a file is renamed (by the filesystem or editor)
|
||||
func notify_file_renamed(old_path: String, new_path: String) -> void:
|
||||
file_renamed.emit(old_path, new_path)
|
||||
|
||||
# Find a file by name in the filesystem
|
||||
func find_file_in_filesystem(dir: EditorFileSystemDirectory, filename: String) -> String:
|
||||
# Check files in current directory
|
||||
for i in range(dir.get_file_count()):
|
||||
var file_path := dir.get_file_path(i)
|
||||
if file_path.get_file() == filename:
|
||||
return file_path
|
||||
|
||||
# Recursively check subdirectories
|
||||
for i in range(dir.get_subdir_count()):
|
||||
var subdir := dir.get_subdir(i)
|
||||
var result := find_file_in_filesystem(subdir, filename)
|
||||
if not result.is_empty():
|
||||
return result
|
||||
|
||||
return ""
|
||||
|
||||
func _refresh_filesystem(path: String) -> void:
|
||||
EditorInterface.get_resource_filesystem().update_file(path)
|
||||
@@ -0,0 +1 @@
|
||||
uid://p8fujg7vubpn
|
||||
@@ -0,0 +1,496 @@
|
||||
@tool
|
||||
class_name YAMLEditorFindReplaceBar extends Control
|
||||
|
||||
signal replace_performed
|
||||
signal replace_all_performed
|
||||
|
||||
@export_category("Find Panel Components")
|
||||
@export var find_input: LineEdit
|
||||
@export var matches_label: Label
|
||||
@export var previous_button: Button
|
||||
@export var next_button: Button
|
||||
@export var match_case_checkbox: CheckBox
|
||||
@export var whole_words_checkbox: CheckBox
|
||||
@export var find_button_container: HBoxContainer
|
||||
@export var find_options_container: HBoxContainer
|
||||
|
||||
@export_category("Replace Panel Components")
|
||||
@export var replace_input: LineEdit
|
||||
@export var replace_button: Button
|
||||
@export var replace_all_button: Button
|
||||
@export var selection_only_checkbox: CheckBox
|
||||
@export var replace_button_container: HBoxContainer
|
||||
@export var replace_options_container: HBoxContainer
|
||||
|
||||
@export_category("Visibility Toggle")
|
||||
@export var hide_button: Button
|
||||
|
||||
@export_category("Node References")
|
||||
@export var editor: YAMLCodeEditor
|
||||
@export var vbox_container: VBoxContainer # Container holding both panels
|
||||
@export var find_panel: Control # First row (find)
|
||||
@export var replace_panel: Control # Second row (replace)
|
||||
|
||||
# Matching state
|
||||
var matches: Array[Vector2i] = [] # Store line/column pairs of matches
|
||||
var current_match_index: int = -1 # Index of current selection in matches array
|
||||
var search_regex: RegEx = RegEx.new()
|
||||
|
||||
func _ready() -> void:
|
||||
# Setup UI
|
||||
previous_button.icon = get_theme_icon("MoveUp", "EditorIcons")
|
||||
next_button.icon = get_theme_icon("MoveDown", "EditorIcons")
|
||||
hide_button.icon = get_theme_icon("Close", "EditorIcons")
|
||||
|
||||
# Connect signals
|
||||
find_input.text_changed.connect(_on_find_input_changed)
|
||||
find_input.text_submitted.connect(_on_find_input_submitted)
|
||||
previous_button.pressed.connect(_on_previous_button_pressed)
|
||||
next_button.pressed.connect(_on_next_button_pressed)
|
||||
match_case_checkbox.toggled.connect(_on_option_changed)
|
||||
whole_words_checkbox.toggled.connect(_on_option_changed)
|
||||
hide_button.pressed.connect(_on_hide_button_pressed)
|
||||
|
||||
replace_button.pressed.connect(_on_replace_button_pressed)
|
||||
replace_all_button.pressed.connect(_on_replace_all_button_pressed)
|
||||
selection_only_checkbox.toggled.connect(_on_option_changed)
|
||||
|
||||
# Disable buttons initially
|
||||
previous_button.disabled = true
|
||||
next_button.disabled = true
|
||||
replace_button.disabled = true
|
||||
replace_all_button.disabled = true
|
||||
|
||||
# Hide by default
|
||||
visible = false
|
||||
|
||||
# Make sure editor preserves selection when focus changes
|
||||
if editor:
|
||||
editor.set_deselect_on_focus_loss_enabled(false)
|
||||
|
||||
var find_panel_visible: bool:
|
||||
get(): return find_input.visible
|
||||
set(value):
|
||||
find_input.visible = value
|
||||
find_button_container.visible = value
|
||||
find_options_container.visible = value
|
||||
|
||||
var replace_panel_visible: bool:
|
||||
get(): return replace_input.visible
|
||||
set(value):
|
||||
replace_input.visible = value
|
||||
replace_button_container.visible = value
|
||||
replace_options_container.visible = value
|
||||
|
||||
# Public methods
|
||||
func show_find_panel() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
visible = true
|
||||
find_panel_visible = true
|
||||
|
||||
# If there's a selection, use it as search text
|
||||
if editor.has_selection():
|
||||
find_input.text = editor.get_selected_text()
|
||||
|
||||
# Run initial search and update UI
|
||||
trigger_search()
|
||||
|
||||
# Focus the search input
|
||||
find_input.grab_focus()
|
||||
find_input.select_all()
|
||||
|
||||
func show_replace_panel() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
visible = true
|
||||
find_panel_visible = true
|
||||
replace_panel_visible = true
|
||||
|
||||
# If there's a selection, use it as search text
|
||||
if editor.has_selection():
|
||||
find_input.text = editor.get_selected_text()
|
||||
|
||||
# Run initial search and update UI
|
||||
trigger_search()
|
||||
|
||||
# Focus the search input
|
||||
find_input.grab_focus()
|
||||
find_input.select_all()
|
||||
|
||||
func hide_panel() -> void:
|
||||
find_panel_visible = false
|
||||
replace_panel_visible = false
|
||||
visible = false
|
||||
|
||||
# Clear search when hiding
|
||||
if is_instance_valid(editor):
|
||||
editor.set_search_text("")
|
||||
editor.set_search_flags(0)
|
||||
editor.queue_redraw()
|
||||
|
||||
# Core functionality
|
||||
func trigger_search() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
# Store old cursor position to find closest match
|
||||
var old_cursor_line = editor.get_caret_line()
|
||||
var old_cursor_column = editor.get_caret_column()
|
||||
|
||||
# Update TextEdit search settings for highlighting
|
||||
var search_text = find_input.text
|
||||
editor.set_search_text(search_text if visible else "")
|
||||
|
||||
var flags = 0
|
||||
if match_case_checkbox.button_pressed:
|
||||
flags |= TextEdit.SEARCH_MATCH_CASE
|
||||
if whole_words_checkbox.button_pressed:
|
||||
flags |= TextEdit.SEARCH_WHOLE_WORDS
|
||||
editor.set_search_flags(flags)
|
||||
|
||||
# Find all matches
|
||||
matches.clear()
|
||||
current_match_index = -1
|
||||
|
||||
if search_text.is_empty():
|
||||
_update_match_label()
|
||||
_update_button_states()
|
||||
return
|
||||
|
||||
# Create regex pattern
|
||||
_create_search_regex(search_text, match_case_checkbox.button_pressed, whole_words_checkbox.button_pressed)
|
||||
|
||||
# Find all matches using regex
|
||||
for line_num in range(editor.get_line_count()):
|
||||
var line_text = editor.get_line(line_num)
|
||||
var search_results = search_regex.search_all(line_text)
|
||||
|
||||
for result in search_results:
|
||||
matches.append(Vector2i(line_num, result.get_start()))
|
||||
|
||||
# Determine which match to select
|
||||
if matches.is_empty():
|
||||
current_match_index = -1
|
||||
else:
|
||||
# Find closest match to current cursor position
|
||||
var best_distance = -1
|
||||
var best_match = 0
|
||||
|
||||
for i in range(matches.size()):
|
||||
var pos = matches[i]
|
||||
|
||||
# Check if this match is after cursor
|
||||
if pos.x > old_cursor_line or (pos.x == old_cursor_line and pos.y >= old_cursor_column):
|
||||
var distance = (pos.x - old_cursor_line) * 1000 + (pos.y - old_cursor_column)
|
||||
if best_distance < 0 or distance < best_distance:
|
||||
best_distance = distance
|
||||
best_match = i
|
||||
|
||||
# If no match after cursor, wrap to first match
|
||||
if best_distance < 0:
|
||||
current_match_index = 0
|
||||
else:
|
||||
current_match_index = best_match
|
||||
|
||||
# Always ensure we have a selected match if there are any matches
|
||||
if matches.size() > 0 and current_match_index == -1:
|
||||
current_match_index = 0
|
||||
|
||||
# Select the current match if appropriate
|
||||
if current_match_index >= 0 and not (selection_only_checkbox.button_pressed and replace_panel.visible):
|
||||
_select_current_match()
|
||||
|
||||
# Update UI
|
||||
_update_match_label()
|
||||
_update_button_states()
|
||||
|
||||
func find_next() -> void:
|
||||
if matches.is_empty() or not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
# Don't navigate if selection only is active
|
||||
if replace_panel.visible and selection_only_checkbox.button_pressed:
|
||||
return
|
||||
|
||||
# Move to next match
|
||||
current_match_index = (current_match_index + 1) % matches.size()
|
||||
_select_current_match()
|
||||
_update_match_label()
|
||||
|
||||
func find_previous() -> void:
|
||||
if matches.is_empty() or not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
# Don't navigate if selection only is active
|
||||
if replace_panel.visible and selection_only_checkbox.button_pressed:
|
||||
return
|
||||
|
||||
# Move to previous match
|
||||
current_match_index = (current_match_index - 1 + matches.size()) % matches.size()
|
||||
_select_current_match()
|
||||
_update_match_label()
|
||||
|
||||
# Helper methods
|
||||
func _create_search_regex(search_text: String, case_sensitive: bool, whole_words: bool) -> void:
|
||||
# Escape special regex characters
|
||||
var pattern = ""
|
||||
for i in range(search_text.length()):
|
||||
var c = search_text[i]
|
||||
# Escape regex special characters
|
||||
if c in "\\.*+?^$[](){}|":
|
||||
pattern += "\\" + c
|
||||
else:
|
||||
pattern += c
|
||||
|
||||
# Add word boundary anchors if needed
|
||||
if whole_words:
|
||||
pattern = "\\b%s\\b" % pattern
|
||||
|
||||
if not case_sensitive:
|
||||
pattern = "(?i)%s" % pattern
|
||||
|
||||
search_regex = RegEx.new()
|
||||
search_regex.compile(pattern)
|
||||
|
||||
func _select_current_match() -> void:
|
||||
if current_match_index < 0 or current_match_index >= matches.size():
|
||||
return
|
||||
|
||||
var match_pos = matches[current_match_index]
|
||||
var search_length = find_input.text.length()
|
||||
|
||||
# Select the text
|
||||
editor.set_caret_line(match_pos.x)
|
||||
editor.set_caret_column(match_pos.y)
|
||||
editor.select(match_pos.x, match_pos.y, match_pos.x, match_pos.y + search_length)
|
||||
|
||||
# Center the view
|
||||
editor.center_viewport_to_caret()
|
||||
|
||||
func _update_match_label() -> void:
|
||||
if not visible:
|
||||
return
|
||||
|
||||
var count = matches.size()
|
||||
|
||||
matches_label.visible = true
|
||||
|
||||
if count > 0:
|
||||
matches_label.modulate = Color.WHITE
|
||||
matches_label.text = "%d of %d matches" % [current_match_index + 1, count]
|
||||
elif not find_input.text.is_empty():
|
||||
matches_label.modulate = EditorInterface.get_editor_settings().get_setting("text_editor/theme/highlighting/brace_mismatch_color")
|
||||
matches_label.text = "No matches"
|
||||
else:
|
||||
matches_label.visible = false
|
||||
matches_label.text = ""
|
||||
|
||||
func _update_button_states() -> void:
|
||||
var has_matches = matches.size() > 0
|
||||
var selection_only_active = replace_panel.visible and selection_only_checkbox.button_pressed
|
||||
|
||||
# Disable navigation buttons if selection only is checked
|
||||
previous_button.disabled = not has_matches or selection_only_active
|
||||
next_button.disabled = not has_matches or selection_only_active
|
||||
|
||||
# Enable/disable replace buttons
|
||||
if selection_only_active and editor.has_selection():
|
||||
var has_matches_in_selection = get_matches_in_selection().size() > 0
|
||||
replace_button.disabled = not has_matches_in_selection
|
||||
replace_all_button.disabled = not has_matches_in_selection
|
||||
else:
|
||||
replace_button.disabled = not has_matches
|
||||
replace_all_button.disabled = not has_matches
|
||||
|
||||
# Get matches within the current selection when Selection Only is active
|
||||
func get_matches_in_selection() -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
|
||||
if not editor.has_selection() or not selection_only_checkbox.button_pressed:
|
||||
return matches.duplicate()
|
||||
|
||||
var search_text = find_input.text
|
||||
var selection_from_line = editor.get_selection_from_line()
|
||||
var selection_from_column = editor.get_selection_from_column()
|
||||
var selection_to_line = editor.get_selection_to_line()
|
||||
var selection_to_column = editor.get_selection_to_column()
|
||||
|
||||
# Convert selection to absolute character index
|
||||
var selection_start_index = _get_absolute_index(selection_from_line, selection_from_column)
|
||||
var selection_end_index = _get_absolute_index(selection_to_line, selection_to_column)
|
||||
|
||||
for match_pos in matches:
|
||||
# Convert match position to absolute character index
|
||||
var match_start_index = _get_absolute_index(match_pos.x, match_pos.y)
|
||||
var match_end_index = match_start_index + search_text.length()
|
||||
|
||||
# Check if match is fully contained in selection
|
||||
if match_start_index >= selection_start_index and match_end_index <= selection_end_index:
|
||||
result.append(match_pos)
|
||||
|
||||
return result
|
||||
|
||||
# Get the next match after the cursor that's inside the selection
|
||||
func get_next_match_in_selection() -> Vector2i:
|
||||
if not editor.has_selection() or not selection_only_checkbox.button_pressed:
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
var matches_in_selection = get_matches_in_selection()
|
||||
if matches_in_selection.is_empty():
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
var cursor_line = editor.get_caret_line()
|
||||
var cursor_column = editor.get_caret_column()
|
||||
|
||||
# Sort matches by position
|
||||
matches_in_selection.sort_custom(func(a, b):
|
||||
if a.x == b.x:
|
||||
return a.y < b.y
|
||||
return a.x < b.x
|
||||
)
|
||||
|
||||
# Find the first match after cursor
|
||||
for match_pos in matches_in_selection:
|
||||
if match_pos.x > cursor_line or (match_pos.x == cursor_line and match_pos.y >= cursor_column):
|
||||
return match_pos
|
||||
|
||||
# If no match after cursor, wrap to first match
|
||||
return matches_in_selection[0]
|
||||
|
||||
func _get_absolute_index(line: int, column: int) -> int:
|
||||
# Calculate absolute character index from line and column
|
||||
var index = 0
|
||||
for i in range(line):
|
||||
index += editor.get_line(i).length() + 1 # +1 for newline
|
||||
|
||||
index += column
|
||||
return index
|
||||
|
||||
# Signal handlers
|
||||
func _on_find_input_changed(_text: String) -> void:
|
||||
trigger_search()
|
||||
|
||||
func _on_find_input_submitted(_text: String) -> void:
|
||||
find_next()
|
||||
|
||||
func _on_option_changed(_toggled: bool) -> void:
|
||||
trigger_search()
|
||||
|
||||
func _on_previous_button_pressed() -> void:
|
||||
find_previous()
|
||||
|
||||
func _on_next_button_pressed() -> void:
|
||||
find_next()
|
||||
|
||||
func _on_hide_button_pressed() -> void:
|
||||
hide_panel()
|
||||
|
||||
func _on_replace_button_pressed() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
var search_text = find_input.text
|
||||
if search_text.is_empty() or matches.is_empty():
|
||||
return
|
||||
|
||||
var replace_text = replace_input.text
|
||||
|
||||
# Different behavior based on Selection Only mode
|
||||
if selection_only_checkbox.button_pressed and editor.has_selection():
|
||||
# Get next match in selection
|
||||
var match_pos = get_next_match_in_selection()
|
||||
if match_pos.x < 0: # No match in selection
|
||||
return
|
||||
|
||||
# Replace the text
|
||||
var line_text = editor.get_line(match_pos.x)
|
||||
var new_line_text = line_text.substr(0, match_pos.y) + replace_text + line_text.substr(match_pos.y + search_text.length())
|
||||
editor.set_line(match_pos.x, new_line_text)
|
||||
|
||||
# Move cursor to the end of the replaced text for next find
|
||||
var cursor_line = match_pos.x
|
||||
var cursor_column = match_pos.y + replace_text.length()
|
||||
|
||||
# Update the document's content
|
||||
editor.text_changed.emit()
|
||||
|
||||
# Refresh search
|
||||
trigger_search()
|
||||
|
||||
# Restore cursor position for next replacement
|
||||
editor.set_caret_line(cursor_line)
|
||||
editor.set_caret_column(cursor_column)
|
||||
else:
|
||||
# Normal replace mode - use the current highlighted match
|
||||
if current_match_index < 0 or current_match_index >= matches.size():
|
||||
return
|
||||
|
||||
# Get current match
|
||||
var match_pos = matches[current_match_index]
|
||||
|
||||
# Replace the text
|
||||
var line_text = editor.get_line(match_pos.x)
|
||||
var new_line_text = line_text.substr(0, match_pos.y) + replace_text + line_text.substr(match_pos.y + search_text.length())
|
||||
editor.set_line(match_pos.x, new_line_text)
|
||||
|
||||
# Update the document's content
|
||||
editor.text_changed.emit()
|
||||
|
||||
# Refresh search
|
||||
trigger_search()
|
||||
|
||||
replace_performed.emit()
|
||||
|
||||
func _on_replace_all_button_pressed() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
var search_text = find_input.text
|
||||
if search_text.is_empty() or matches.is_empty():
|
||||
return
|
||||
|
||||
var replace_text = replace_input.text
|
||||
|
||||
# Determine which matches to replace
|
||||
var matches_to_replace: Array[Vector2i]
|
||||
|
||||
if selection_only_checkbox.button_pressed and editor.has_selection():
|
||||
matches_to_replace = get_matches_in_selection()
|
||||
else:
|
||||
matches_to_replace = matches.duplicate()
|
||||
|
||||
if matches_to_replace.is_empty():
|
||||
return
|
||||
|
||||
# Sort matches in reverse order (to not affect positions of earlier matches)
|
||||
matches_to_replace.sort_custom(func(a, b):
|
||||
if a.x == b.x:
|
||||
return a.y > b.y
|
||||
return a.x > b.x
|
||||
)
|
||||
|
||||
# Process replacements
|
||||
var lines = editor.text.split("\n", false)
|
||||
var replacements_count = 0
|
||||
|
||||
for match_pos in matches_to_replace:
|
||||
# Replace text in the line
|
||||
var line_text = lines[match_pos.x]
|
||||
lines[match_pos.x] = line_text.substr(0, match_pos.y) + replace_text + line_text.substr(match_pos.y + search_text.length())
|
||||
replacements_count += 1
|
||||
|
||||
# Only update if we made changes
|
||||
if replacements_count > 0:
|
||||
# Set the new text
|
||||
editor.text = "\n".join(lines)
|
||||
|
||||
# Update the document's content
|
||||
editor.text_changed.emit()
|
||||
|
||||
# Refresh search
|
||||
trigger_search()
|
||||
|
||||
replace_all_performed.emit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bep06otwjntx1
|
||||
@@ -0,0 +1,91 @@
|
||||
@tool
|
||||
class_name YAMLEditorMenuBar extends MenuBar
|
||||
|
||||
signal new_file
|
||||
signal open_file
|
||||
signal save_requested
|
||||
signal save_as_requested
|
||||
signal close_requested
|
||||
|
||||
signal undo_requested
|
||||
signal redo_requested
|
||||
|
||||
signal cut_requested
|
||||
signal copy_requested
|
||||
signal paste_requested
|
||||
signal select_all_requested
|
||||
|
||||
signal find_requested
|
||||
signal find_next_requested
|
||||
signal find_previous_requested
|
||||
signal replace_requested
|
||||
|
||||
@export var file_menu: PopupMenu
|
||||
@export var edit_menu: PopupMenu
|
||||
@export var search_menu: PopupMenu
|
||||
|
||||
func _ready() -> void:
|
||||
# Wait for UI to be ready
|
||||
await get_tree().process_frame
|
||||
|
||||
# Set up the menu bar
|
||||
_setup_menus()
|
||||
|
||||
func _setup_menus() -> void:
|
||||
# File menu
|
||||
file_menu.clear()
|
||||
file_menu.add_item("New", 0, KEY_MASK_CTRL | KEY_N)
|
||||
file_menu.add_item("Open...", 1, KEY_MASK_CTRL | KEY_O)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("Save", 2, KEY_MASK_CTRL | KEY_S)
|
||||
file_menu.add_item("Save As...", 3, KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_S)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("Close", 4, KEY_MASK_CTRL | KEY_W)
|
||||
|
||||
# Edit menu
|
||||
edit_menu.clear()
|
||||
edit_menu.add_item("Undo", 0, KEY_MASK_CTRL | KEY_Z)
|
||||
edit_menu.add_item("Redo", 1, KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_Z)
|
||||
edit_menu.add_separator()
|
||||
edit_menu.add_item("Cut", 2, KEY_MASK_CTRL | KEY_X)
|
||||
edit_menu.add_item("Copy", 3, KEY_MASK_CTRL | KEY_C)
|
||||
edit_menu.add_item("Paste", 4, KEY_MASK_CTRL | KEY_V)
|
||||
edit_menu.add_separator()
|
||||
edit_menu.add_item("Select All", 5, KEY_MASK_CTRL | KEY_A)
|
||||
|
||||
# Search menu
|
||||
search_menu.clear()
|
||||
search_menu.add_item("Find...", 0, KEY_MASK_CTRL | KEY_F)
|
||||
search_menu.add_item("Find Next", 1, KEY_F3)
|
||||
search_menu.add_item("Find Previous", 2, KEY_MASK_SHIFT | KEY_F3)
|
||||
search_menu.add_separator()
|
||||
search_menu.add_item("Replace...", 3, KEY_MASK_CTRL | KEY_R)
|
||||
|
||||
# Connect signals
|
||||
file_menu.id_pressed.connect(_on_file_menu_id_pressed)
|
||||
edit_menu.id_pressed.connect(_on_edit_menu_id_pressed)
|
||||
search_menu.id_pressed.connect(_on_search_menu_id_pressed)
|
||||
|
||||
func _on_file_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
0: new_file.emit()
|
||||
1: open_file.emit()
|
||||
2: save_requested.emit()
|
||||
3: save_as_requested.emit()
|
||||
4: close_requested.emit()
|
||||
|
||||
func _on_edit_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
0: undo_requested.emit()
|
||||
1: redo_requested.emit()
|
||||
2: cut_requested.emit()
|
||||
3: copy_requested.emit()
|
||||
4: paste_requested.emit()
|
||||
5: select_all_requested.emit()
|
||||
|
||||
func _on_search_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
0: find_requested.emit()
|
||||
1: find_next_requested.emit()
|
||||
2: find_previous_requested.emit()
|
||||
3: replace_requested.emit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b35lyu1onhcoj
|
||||
@@ -0,0 +1,109 @@
|
||||
@tool
|
||||
class_name YAMLEditorSessionManager extends Node
|
||||
|
||||
const CONFIG_PATH := "res://.godot/yaml_editor_session.cfg"
|
||||
const CONFIG_SECTION := "yaml_editor"
|
||||
const CONFIG_KEY_OPEN_FILES := "open_files"
|
||||
const CONFIG_KEY_SPLIT_OFFSET := "split_offset"
|
||||
const CONFIG_KEY_CURRENT_FILE := "current_file"
|
||||
|
||||
var file_manager: YAMLEditorDocumentManager
|
||||
var file_system: YAMLFileSystem
|
||||
var config: ConfigFile
|
||||
var autosave_timer: Timer
|
||||
var resizable_container: HSplitContainer
|
||||
|
||||
func _ready() -> void:
|
||||
file_system = YAMLFileSystem.get_singleton()
|
||||
|
||||
config = ConfigFile.new()
|
||||
|
||||
# Setup autosave timer
|
||||
autosave_timer = Timer.new()
|
||||
add_child(autosave_timer)
|
||||
autosave_timer.wait_time = 10.0 # Save session every 10 seconds
|
||||
autosave_timer.one_shot = false
|
||||
autosave_timer.autostart = true
|
||||
autosave_timer.timeout.connect(_on_autosave_timer_timeout)
|
||||
|
||||
func setup(p_file_manager: YAMLEditorDocumentManager, p_resizable_container: HSplitContainer) -> void:
|
||||
file_manager = p_file_manager
|
||||
resizable_container = p_resizable_container
|
||||
|
||||
# Connect to signals
|
||||
file_manager.document_changed.connect(_on_session_changed)
|
||||
file_manager.document_created.connect(_on_session_changed)
|
||||
file_manager.document_closed.connect(_on_session_changed)
|
||||
resizable_container.dragged.connect(_on_split_dragged)
|
||||
|
||||
func _on_split_dragged(_offset: int) -> void:
|
||||
# The split position has changed, save the session
|
||||
_on_session_changed()
|
||||
|
||||
func save_session() -> void:
|
||||
# Don't save anything if we have no files
|
||||
if not is_instance_valid(file_manager):
|
||||
return
|
||||
|
||||
var documents: Array = file_manager.get_open_documents()
|
||||
|
||||
# Create array of persistent file paths (skip untitled files)
|
||||
var persistent_files: PackedStringArray = []
|
||||
for document in documents:
|
||||
if not document.is_untitled():
|
||||
persistent_files.append(document.path)
|
||||
|
||||
# Get current file path
|
||||
var current_path := ""
|
||||
var current_document := file_manager.get_current_document()
|
||||
if current_document and not current_document.is_untitled():
|
||||
current_path = current_document.path
|
||||
|
||||
# Save to config file
|
||||
config.set_value(CONFIG_SECTION, CONFIG_KEY_OPEN_FILES, persistent_files)
|
||||
config.set_value(CONFIG_SECTION, CONFIG_KEY_CURRENT_FILE, current_path)
|
||||
|
||||
# Save the split offset
|
||||
if is_instance_valid(resizable_container):
|
||||
config.set_value(CONFIG_SECTION, CONFIG_KEY_SPLIT_OFFSET, resizable_container.split_offset)
|
||||
|
||||
var error := config.save(CONFIG_PATH)
|
||||
if error != OK:
|
||||
push_error("Failed to save YAML editor session: %s" % error_string(error))
|
||||
|
||||
func load_session() -> void:
|
||||
var error := config.load(CONFIG_PATH)
|
||||
if error != OK:
|
||||
# No saved session or error loading it
|
||||
if error != ERR_FILE_NOT_FOUND:
|
||||
push_error("Failed to load YAML editor session: ", error_string(error))
|
||||
return
|
||||
|
||||
# Get saved file paths
|
||||
var file_paths: PackedStringArray = config.get_value(CONFIG_SECTION, CONFIG_KEY_OPEN_FILES, [])
|
||||
|
||||
# Open each file
|
||||
for path in file_paths:
|
||||
if file_system.file_exists(path):
|
||||
file_manager.open_file(path)
|
||||
|
||||
# Set current file
|
||||
var last_current: String = config.get_value(CONFIG_SECTION, CONFIG_KEY_CURRENT_FILE, "")
|
||||
if not last_current.is_empty() and file_manager.has_document(last_current):
|
||||
var document := file_manager.get_document(last_current)
|
||||
file_manager.set_current_document(document)
|
||||
|
||||
# Restore split offset (deferred to ensure UI is ready)
|
||||
call_deferred("_restore_split_offset")
|
||||
|
||||
func _restore_split_offset() -> void:
|
||||
if is_instance_valid(resizable_container):
|
||||
var saved_offset: int = config.get_value(CONFIG_SECTION, CONFIG_KEY_SPLIT_OFFSET, resizable_container.split_offset)
|
||||
resizable_container.split_offset = saved_offset
|
||||
|
||||
func _on_session_changed(_document = null) -> void:
|
||||
# Set a short timer to prevent saving too frequently during batch operations
|
||||
autosave_timer.start()
|
||||
|
||||
func _on_autosave_timer_timeout() -> void:
|
||||
save_session()
|
||||
@@ -0,0 +1 @@
|
||||
uid://c3hnw5vrco2iy
|
||||
@@ -0,0 +1,64 @@
|
||||
@tool
|
||||
class_name YAMLEditorStatusBar extends HBoxContainer
|
||||
|
||||
@export var editor: YAMLCodeEditor
|
||||
@export var status_label: Label
|
||||
@export var zoom_button: Button
|
||||
@export var line_column_label: Label
|
||||
|
||||
var zoom_popup_menu: PopupMenu
|
||||
|
||||
func _ready() -> void:
|
||||
# Zoom popup menu
|
||||
zoom_popup_menu = PopupMenu.new()
|
||||
add_child(zoom_popup_menu)
|
||||
zoom_popup_menu.add_item("25 %", 0)
|
||||
zoom_popup_menu.add_item("50 %", 1)
|
||||
zoom_popup_menu.add_item("75 %", 2)
|
||||
zoom_popup_menu.add_item("100 %", 3)
|
||||
zoom_popup_menu.add_item("150 %", 4)
|
||||
zoom_popup_menu.add_item("200 %", 5)
|
||||
zoom_popup_menu.add_item("300 %", 6)
|
||||
zoom_popup_menu.id_pressed.connect(_on_zoom_popup_menu_id_pressed)
|
||||
zoom_button.pressed.connect(
|
||||
func():
|
||||
var global_rect := Rect2(get_global_mouse_position(), Vector2.ZERO)
|
||||
zoom_popup_menu.popup_on_parent(global_rect)
|
||||
)
|
||||
|
||||
status_label.set("theme_override_constants/use_pixel_snap", true)
|
||||
|
||||
func _on_zoom_popup_menu_id_pressed(idx: int) -> void:
|
||||
match idx:
|
||||
0: editor.set_zoom(0.25)
|
||||
1: editor.set_zoom(0.5)
|
||||
2: editor.set_zoom(0.75)
|
||||
3: editor.set_zoom(1.0)
|
||||
4: editor.set_zoom(1.5)
|
||||
5: editor.set_zoom(2.0)
|
||||
6: editor.set_zoom(3.0)
|
||||
zoom_popup_menu.hide()
|
||||
|
||||
func set_status(text: String, color := Color.WHITE) -> void:
|
||||
status_label.text = text
|
||||
status_label.modulate = color
|
||||
|
||||
func set_line_column(line_column: Array[int]) -> void:
|
||||
var line := line_column[0]
|
||||
var col := line_column[1]
|
||||
line_column_label.text = "%d : %d" % [line, col]
|
||||
|
||||
func set_zoom_level(level: float) -> void:
|
||||
zoom_button.text = str(int(level * 100)) + " %"
|
||||
|
||||
func set_validation_result(result: YAMLResult) -> void:
|
||||
if !result.has_error():
|
||||
return set_status("")
|
||||
|
||||
var error := result.get_error_message()
|
||||
var line := result.get_error_line()
|
||||
var col := result.get_error_column()
|
||||
var error_text := "Error at (%d, %d): %s" % [line, col, error] if line >= 0 else "Error: %s" % error
|
||||
|
||||
var error_color: Color = EditorInterface.get_editor_settings().get_setting("text_editor/theme/highlighting/brace_mismatch_color")
|
||||
set_status(error_text, error_color)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkanguis54ea3
|
||||
@@ -0,0 +1,21 @@
|
||||
@tool
|
||||
class_name YAMLEditorSyntaxHighlighter extends EditorSyntaxHighlighter
|
||||
|
||||
var cache: Dictionary = {}
|
||||
var syntax_parser := YAMLSyntaxParser.new()
|
||||
var color_provider := YAMLSyntaxParser.ColorProvider.new()
|
||||
|
||||
func clear_highlighting_cache() -> void:
|
||||
cache.clear()
|
||||
|
||||
func _get_line_syntax_highlighting(line: int) -> Dictionary:
|
||||
var text: String = get_text_edit().get_line(line)
|
||||
if text in cache:
|
||||
return cache[text]
|
||||
|
||||
color_provider.update_theme()
|
||||
cache[text] = _highlight_line(text)
|
||||
return cache[text]
|
||||
|
||||
func _highlight_line(text: String) -> Dictionary:
|
||||
return syntax_parser.highlight_line(text, color_provider)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0q2685eddg35
|
||||
@@ -0,0 +1,20 @@
|
||||
class_name YAMLSyntaxHighlighter extends SyntaxHighlighter
|
||||
|
||||
var cache: Dictionary = {}
|
||||
var syntax_parser := YAMLSyntaxParser.new()
|
||||
var color_provider := YAMLSyntaxParser.ColorProvider.new()
|
||||
|
||||
func clear_highlighting_cache() -> void:
|
||||
cache.clear()
|
||||
|
||||
func _get_line_syntax_highlighting(line: int) -> Dictionary:
|
||||
var text: String = get_text_edit().get_line(line)
|
||||
if text in cache:
|
||||
return cache[text]
|
||||
|
||||
color_provider.update_theme()
|
||||
cache[text] = _highlight_line(text)
|
||||
return cache[text]
|
||||
|
||||
func _highlight_line(text: String) -> Dictionary:
|
||||
return syntax_parser.highlight_line(text, color_provider)
|
||||
@@ -0,0 +1 @@
|
||||
uid://8yvnf7wlu3ud
|
||||
@@ -0,0 +1,384 @@
|
||||
@tool
|
||||
class_name YAMLSyntaxParser extends RefCounted
|
||||
|
||||
## Token types
|
||||
enum TokenType {
|
||||
TEXT, # For keys only
|
||||
COMMENT, # Comments
|
||||
SYMBOL, # Structural elements like :, -, >, |, &, *, [, ], {, }
|
||||
STRING, # String values (default for unmatched values)
|
||||
NUMBER, # Numeric values
|
||||
KEYWORD, # Booleans, null, merge keys, tags
|
||||
DOCUMENT_SEPARATOR, # New document separator
|
||||
}
|
||||
|
||||
var re_patterns := {
|
||||
"merge_key": RegEx.create_from_string("^\\s*<<:\\s*\\*[^\\s]+"),
|
||||
"multiline_indicator": RegEx.create_from_string("(>|\\|-?)\\s*$"),
|
||||
"array_item": RegEx.create_from_string("^(\\s*-(?:\\s*-)*\\s*)(.*)$"),
|
||||
"key_value": RegEx.create_from_string("^\\s*([^:]+):(.*)$"),
|
||||
|
||||
# Scalar patterns
|
||||
"quoted_string": RegEx.create_from_string("^(['\"])(?:\\\\.|[^\\\\])*\\1$"),
|
||||
"number": RegEx.create_from_string("^(?:0[xX][0-9a-fA-F]+|0[oO][0-7]+|0[bB][0-1]+|[-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][-+]?\\d+)?)$"),
|
||||
"boolean": RegEx.create_from_string("^(true|false)$"),
|
||||
"nullish": RegEx.create_from_string("^(null|~)$"),
|
||||
"special": RegEx.create_from_string("^(\\.inf|\\.nan)$"),
|
||||
|
||||
# YAML functionality
|
||||
"anchor": RegEx.create_from_string("^\\s*&([^\\s]+)"),
|
||||
"alias": RegEx.create_from_string("^\\s*\\*([^\\s]+)"),
|
||||
"tag": RegEx.create_from_string("!(?:![\\w\\-\\.]+|[^\\s\\[\\]{}'\"`]+)"),
|
||||
"top_level_tag": RegEx.create_from_string("^\\s*(!(?:![\\w\\-\\.]+|[^\\s\\[\\]{}'\"`]+))\\s*(.*)$"),
|
||||
"document_separator": RegEx.create_from_string("^---$")
|
||||
}
|
||||
|
||||
class ParserState:
|
||||
var in_string: bool = false
|
||||
var string_char: String = ""
|
||||
var stack: Array = [] # For nested flow collections
|
||||
var token_start: int = -1
|
||||
var colors: Dictionary = {}
|
||||
|
||||
func push(char: String) -> void:
|
||||
stack.push_back(char)
|
||||
|
||||
func pop() -> String:
|
||||
return stack.pop_back() if not stack.is_empty() else ""
|
||||
|
||||
func peek() -> String:
|
||||
return stack.back() if not stack.is_empty() else ""
|
||||
|
||||
## Provides colors to the syntax highlighter
|
||||
class ColorProvider:
|
||||
# Default theme colors
|
||||
var theme: Dictionary = {
|
||||
"text": Color(0.8025, 0.81, 0.8225, 1),
|
||||
"comment": Color(0.8025, 0.81, 0.8225, 0.5),
|
||||
"symbol": Color(0.67, 0.79, 1, 1),
|
||||
"string": Color(1, 0.93, 0.63, 1),
|
||||
"number": Color(0.63, 1, 0.88, 1),
|
||||
"keyword": Color(1, 0.44, 0.52, 1),
|
||||
"document_separator": Color(0.8025, 0.81, 0.8225, 0.5)
|
||||
}
|
||||
|
||||
func _init():
|
||||
update_theme()
|
||||
|
||||
## Updates the theme colors from Godot Editor settings
|
||||
func update_theme() -> void:
|
||||
# Only inside the Godot Editor
|
||||
if !Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Read theme from editor settings
|
||||
var settings = EditorInterface.get_editor_settings()
|
||||
theme = {
|
||||
"text": settings.get_setting("text_editor/theme/highlighting/text_color"),
|
||||
"comment": settings.get_setting("text_editor/theme/highlighting/comment_color"),
|
||||
"symbol": settings.get_setting("text_editor/theme/highlighting/symbol_color"),
|
||||
"string": settings.get_setting("text_editor/theme/highlighting/string_color"),
|
||||
"number": settings.get_setting("text_editor/theme/highlighting/number_color"),
|
||||
"keyword": settings.get_setting("text_editor/theme/highlighting/keyword_color"),
|
||||
"document_separator": settings.get_setting("text_editor/theme/highlighting/comment_color"),
|
||||
}
|
||||
|
||||
## Get color for tokens
|
||||
func get_color_for_type(type: TokenType) -> Color:
|
||||
match type:
|
||||
YAMLSyntaxParser.TokenType.TEXT: return theme.text
|
||||
YAMLSyntaxParser.TokenType.COMMENT: return theme.comment
|
||||
YAMLSyntaxParser.TokenType.SYMBOL: return theme.symbol
|
||||
YAMLSyntaxParser.TokenType.STRING: return theme.string
|
||||
YAMLSyntaxParser.TokenType.NUMBER: return theme.number
|
||||
YAMLSyntaxParser.TokenType.KEYWORD: return theme.keyword
|
||||
YAMLSyntaxParser.TokenType.DOCUMENT_SEPARATOR: return theme.document_separator
|
||||
_: return theme.string # Default fallback is string color
|
||||
|
||||
## Highlight a line of YAML
|
||||
func highlight_line(text: String, color_provider: ColorProvider = ColorProvider.new()) -> Dictionary:
|
||||
var comment_pos := _find_comment_start(text)
|
||||
var content := text if comment_pos == -1 else text.substr(0, comment_pos).rstrip(" \t")
|
||||
var colors := {}
|
||||
|
||||
if content.strip_edges():
|
||||
colors = _highlight_line_content(content, color_provider)
|
||||
|
||||
if comment_pos != -1:
|
||||
colors[comment_pos] = {"color": color_provider.get_color_for_type(TokenType.COMMENT)}
|
||||
|
||||
return _sort_colors(colors)
|
||||
|
||||
# Find the beginning position of a comment
|
||||
func _find_comment_start(text: String) -> int:
|
||||
var in_string := false
|
||||
var string_char := ""
|
||||
|
||||
for i in range(text.length()):
|
||||
var char := text[i]
|
||||
|
||||
if not in_string:
|
||||
if char in ['"', "'"] and (i == 0 or text[i - 1] != '\\'):
|
||||
in_string = true
|
||||
string_char = char
|
||||
elif char == '#' and (i == 0 or text[i - 1] == ' ' or text[i - 1] == '\t'):
|
||||
return i
|
||||
else:
|
||||
if char == string_char and (i == 0 or text[i - 1] != '\\'):
|
||||
in_string = false
|
||||
string_char = ""
|
||||
|
||||
return -1
|
||||
|
||||
# Highlight line content
|
||||
func _highlight_line_content(text: String, color_provider: ColorProvider) -> Dictionary:
|
||||
# Handle document separator
|
||||
var separator_match: RegExMatch = re_patterns.document_separator.search(text)
|
||||
if separator_match:
|
||||
return {0: {"color": color_provider.get_color_for_type(TokenType.DOCUMENT_SEPARATOR)}}
|
||||
|
||||
# Handle merge keys
|
||||
var merge_match: RegExMatch = re_patterns.merge_key.search(text)
|
||||
if merge_match:
|
||||
return {
|
||||
merge_match.get_start(0): {"color": color_provider.get_color_for_type(TokenType.KEYWORD)}
|
||||
}
|
||||
|
||||
# Check for top-level tags
|
||||
var tag_match: RegExMatch = re_patterns.top_level_tag.search(text)
|
||||
if tag_match:
|
||||
var colors := {}
|
||||
# Color just the tag part as keyword (red)
|
||||
_add_color(color_provider, colors, tag_match.get_start(1), tag_match.get_end(1), TokenType.KEYWORD)
|
||||
|
||||
# Process any remaining content after the tag
|
||||
var remaining = tag_match.get_string(2).strip_edges()
|
||||
if remaining:
|
||||
var remaining_start = text.find(remaining, tag_match.get_end(1))
|
||||
if remaining_start != -1:
|
||||
if remaining.begins_with("{") or remaining.begins_with("["):
|
||||
colors.merge(_parse_flow_style(color_provider, remaining, remaining_start))
|
||||
else:
|
||||
_add_scalar_color(color_provider, colors, remaining, remaining_start)
|
||||
return colors
|
||||
|
||||
# Handle array items
|
||||
var array_match: RegExMatch = re_patterns.array_item.search(text)
|
||||
if array_match:
|
||||
var colors := {}
|
||||
|
||||
# Color the entire dash section as symbols
|
||||
_add_color(color_provider, colors, array_match.get_start(1), array_match.get_end(1), TokenType.SYMBOL)
|
||||
|
||||
# Process the content after the dashes
|
||||
var content: String = array_match.get_string(2).strip_edges()
|
||||
if content:
|
||||
var content_start: int = array_match.get_start(2)
|
||||
if content.begins_with("[") or content.begins_with("{"):
|
||||
colors.merge(_parse_flow_style(color_provider, content, content_start))
|
||||
else:
|
||||
_add_scalar_color(color_provider, colors, content, content_start)
|
||||
return colors
|
||||
|
||||
# Handle regular key-value pairs
|
||||
var key_value_match: RegExMatch = re_patterns.key_value.search(text)
|
||||
if key_value_match:
|
||||
return _parse_key_value(color_provider, text, key_value_match)
|
||||
|
||||
# Handle flow-style collections at the root level
|
||||
if "[" in text or "{" in text:
|
||||
return _parse_flow_style(color_provider, text, 0)
|
||||
|
||||
# Handle multi-line string indicators
|
||||
var multiline_match: RegExMatch = re_patterns.multiline_indicator.search(text)
|
||||
if multiline_match:
|
||||
var colors := {}
|
||||
# Color the indicator (> or |) as symbol
|
||||
_add_color(color_provider, colors, multiline_match.get_start(1), multiline_match.get_end(1), TokenType.SYMBOL)
|
||||
return colors
|
||||
|
||||
# Default case: treat as string content (for multi-line string content)
|
||||
if text.strip_edges():
|
||||
return {0: {"color": color_provider.get_color_for_type(TokenType.STRING)}}
|
||||
|
||||
return {}
|
||||
|
||||
# Parse flow collections
|
||||
func _parse_flow_style(color_provider: ColorProvider, text: String, offset: int) -> Dictionary:
|
||||
var state := ParserState.new()
|
||||
var pos := 0
|
||||
|
||||
while pos < text.length():
|
||||
var char := text[pos]
|
||||
|
||||
# Handle string literals
|
||||
if char in ['"', "'"] and (pos == 0 or text[pos - 1] != '\\'):
|
||||
if not state.in_string:
|
||||
state.in_string = true
|
||||
state.string_char = char
|
||||
state.token_start = pos
|
||||
elif char == state.string_char:
|
||||
state.in_string = false
|
||||
_add_color(color_provider, state.colors, offset + state.token_start, offset + pos + 1, TokenType.STRING)
|
||||
state.token_start = -1
|
||||
|
||||
# Handle flow collection brackets when not in string
|
||||
elif not state.in_string:
|
||||
if char in ['[', '{']:
|
||||
state.push(char)
|
||||
_add_color(color_provider, state.colors, offset + pos, offset + pos + 1, TokenType.SYMBOL)
|
||||
state.token_start = pos + 1
|
||||
|
||||
elif char in [']', '}']:
|
||||
var matching := '[' if char == ']' else '{'
|
||||
if state.peek() == matching:
|
||||
state.pop()
|
||||
if state.token_start != -1:
|
||||
var token := text.substr(state.token_start, pos - state.token_start).strip_edges()
|
||||
if token:
|
||||
_add_scalar_color(color_provider, state.colors, token, offset + state.token_start)
|
||||
_add_color(color_provider, state.colors, offset + pos, offset + pos + 1, TokenType.SYMBOL)
|
||||
state.token_start = -1
|
||||
|
||||
elif char in [':', ',']:
|
||||
if state.token_start != -1:
|
||||
var token := text.substr(state.token_start, pos - state.token_start).strip_edges()
|
||||
if token:
|
||||
if char == ':':
|
||||
# All map keys should be text colored, regardless of content
|
||||
_add_color(color_provider, state.colors, offset + state.token_start, offset + pos, TokenType.TEXT)
|
||||
else:
|
||||
_add_scalar_color(color_provider, state.colors, token, offset + state.token_start)
|
||||
_add_color(color_provider, state.colors, offset + pos, offset + pos + 1, TokenType.SYMBOL)
|
||||
state.token_start = pos + 1
|
||||
|
||||
elif char != ' ' and state.token_start == -1:
|
||||
state.token_start = pos
|
||||
|
||||
pos += 1
|
||||
|
||||
# Handle any remaining token
|
||||
if state.token_start != -1 and state.token_start < pos:
|
||||
var token := text.substr(state.token_start, pos - state.token_start).strip_edges()
|
||||
if token:
|
||||
# Check if this is a key in a map context
|
||||
if not state.stack.is_empty() and state.stack.back() == '{' and ':' in text.substr(pos):
|
||||
_add_color(color_provider, state.colors, offset + state.token_start, offset + pos, TokenType.TEXT)
|
||||
else:
|
||||
_add_scalar_color(color_provider, state.colors, token, offset + state.token_start)
|
||||
|
||||
return state.colors
|
||||
|
||||
# Parse dictionary key and value
|
||||
func _parse_key_value(color_provider: ColorProvider, text: String, match: RegExMatch) -> Dictionary:
|
||||
var colors := {}
|
||||
|
||||
# Color the key
|
||||
_add_color(color_provider, colors, match.get_start(1), match.get_end(1), TokenType.TEXT)
|
||||
|
||||
# Color the colon
|
||||
_add_color(color_provider,colors, match.get_end(1), match.get_end(1) + 1, TokenType.SYMBOL)
|
||||
|
||||
# Get and process the value if present
|
||||
var value := match.get_string(2).strip_edges()
|
||||
if value:
|
||||
var value_start := text.find(value, match.get_end(1))
|
||||
if value_start != -1:
|
||||
# First check for and handle any tags
|
||||
var tag_match: RegExMatch = re_patterns.tag.search(value)
|
||||
if tag_match:
|
||||
_add_color(color_provider, colors, value_start + tag_match.get_start(0),
|
||||
value_start + tag_match.get_end(0), TokenType.KEYWORD)
|
||||
# Get remaining content after tag
|
||||
var after_tag := value.substr(tag_match.get_end(0)).strip_edges()
|
||||
if after_tag:
|
||||
var after_tag_start = text.find(after_tag, value_start + tag_match.get_end(0))
|
||||
if after_tag_start != -1:
|
||||
# Now check for multiline indicator in remaining content
|
||||
var indicator_match: RegExMatch = re_patterns.multiline_indicator.search(after_tag)
|
||||
if indicator_match:
|
||||
_add_color(color_provider, colors, after_tag_start + indicator_match.get_start(1), after_tag_start + indicator_match.get_end(1), TokenType.SYMBOL)
|
||||
elif after_tag.begins_with("{") or after_tag.begins_with("["):
|
||||
# Process flow style collections after the tag
|
||||
colors.merge(_parse_flow_style(color_provider, after_tag, after_tag_start))
|
||||
else:
|
||||
# Process normal scalar after the tag
|
||||
_add_scalar_color(color_provider, colors, after_tag, after_tag_start)
|
||||
return colors
|
||||
|
||||
# If no tag, check for multiline indicator in full value
|
||||
var indicator_match: RegExMatch = re_patterns.multiline_indicator.search(value)
|
||||
if indicator_match:
|
||||
_add_color(color_provider, colors, value_start + indicator_match.get_start(1), value_start + indicator_match.get_end(1), TokenType.SYMBOL)
|
||||
elif value.begins_with("[") or value.begins_with("{"):
|
||||
colors.merge(_parse_flow_style(color_provider, value, value_start))
|
||||
else:
|
||||
_add_scalar_color(color_provider, colors, value, value_start)
|
||||
return colors
|
||||
|
||||
# Colors for scalar values
|
||||
func _add_scalar_color(color_provider: ColorProvider, colors: Dictionary, token: String, start_index: int) -> void:
|
||||
# Handle empty or whitespace-only tokens
|
||||
token = token.strip_edges()
|
||||
if token.is_empty():
|
||||
return
|
||||
|
||||
# Check for quoted strings first
|
||||
if re_patterns.quoted_string.search(token):
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.STRING)
|
||||
return # Important: return early to prevent parsing tags inside strings
|
||||
|
||||
# Check for tags
|
||||
var tag_match: RegExMatch = re_patterns.tag.search(token)
|
||||
if tag_match:
|
||||
var tag_start := tag_match.get_start(0)
|
||||
var tag_end := tag_match.get_end(0)
|
||||
|
||||
# Only color the tag portion
|
||||
_add_color(color_provider, colors, start_index + tag_start, start_index + tag_end, TokenType.KEYWORD)
|
||||
|
||||
# Process any remaining content after the tag
|
||||
if tag_end < token.length():
|
||||
var remaining := token.substr(tag_end).strip_edges()
|
||||
if remaining:
|
||||
var remaining_start = start_index + token.find(remaining, tag_end)
|
||||
if remaining_start != -1:
|
||||
if remaining.begins_with("{") or remaining.begins_with("["):
|
||||
colors.merge(_parse_flow_style(color_provider, remaining, remaining_start))
|
||||
else:
|
||||
# Apply appropriate coloring for the remaining content
|
||||
if re_patterns.number.search(remaining):
|
||||
_add_color(color_provider, colors, remaining_start, remaining_start + remaining.length(), TokenType.NUMBER)
|
||||
elif re_patterns.boolean.search(remaining) or re_patterns.nullish.search(remaining) or re_patterns.special.search(remaining):
|
||||
_add_color(color_provider, colors, remaining_start, remaining_start + remaining.length(), TokenType.KEYWORD)
|
||||
else:
|
||||
_add_color(color_provider, colors, remaining_start, remaining_start + remaining.length(), TokenType.STRING)
|
||||
return
|
||||
|
||||
# Rest of the scalar checks for non-tag content
|
||||
elif re_patterns.number.search(token):
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.NUMBER)
|
||||
elif re_patterns.boolean.search(token) or re_patterns.nullish.search(token) or re_patterns.special.search(token):
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.KEYWORD)
|
||||
elif re_patterns.anchor.search(token) or re_patterns.alias.search(token):
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.SYMBOL)
|
||||
else:
|
||||
# Default fallback is string color
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.STRING)
|
||||
|
||||
# Add color for a type
|
||||
func _add_color(color_provider: ColorProvider, colors: Dictionary, start: int, end: int, type: TokenType) -> void:
|
||||
colors[start] = {"color": color_provider.get_color_for_type(type)}
|
||||
|
||||
# Sort the colors dictionary by index
|
||||
func _sort_colors(colors: Dictionary) -> Dictionary:
|
||||
# Get all indices as an array
|
||||
var indices := colors.keys()
|
||||
indices.sort() # Sort indices in ascending order
|
||||
|
||||
# Create new dictionary with sorted indices
|
||||
var sorted_colors := {}
|
||||
for idx in indices:
|
||||
sorted_colors[idx] = colors[idx]
|
||||
|
||||
return sorted_colors
|
||||
@@ -0,0 +1 @@
|
||||
uid://dco608883k7yl
|
||||
@@ -0,0 +1,109 @@
|
||||
@tool
|
||||
class_name YAMLEditorValidator extends Node
|
||||
|
||||
signal validation_completed(document)
|
||||
|
||||
var _thread: Thread
|
||||
var _is_validating: bool = false
|
||||
var _pending_validation: bool = false
|
||||
var _validation_queue: Array = []
|
||||
|
||||
var code_editor: YAMLCodeEditor
|
||||
var validation_timer: Timer
|
||||
var file_system: YAMLFileSystem
|
||||
var file_manager: YAMLEditorDocumentManager
|
||||
|
||||
func _ready() -> void:
|
||||
file_system = YAMLFileSystem.get_singleton()
|
||||
|
||||
# Create validation timer
|
||||
validation_timer = Timer.new()
|
||||
add_child(validation_timer)
|
||||
validation_timer.one_shot = true
|
||||
validation_timer.wait_time = 0.5 # 500ms delay
|
||||
validation_timer.timeout.connect(_on_validation_timer_timeout)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _thread and _thread.is_started():
|
||||
_thread.wait_to_finish()
|
||||
|
||||
func setup(p_code_editor: YAMLCodeEditor, p_file_manager: YAMLEditorDocumentManager) -> void:
|
||||
code_editor = p_code_editor
|
||||
file_manager = p_file_manager
|
||||
|
||||
# Connect to code editor changes
|
||||
code_editor.validation_requested.connect(_on_validation_requested)
|
||||
|
||||
# Connect to document changes
|
||||
file_manager.document_changed.connect(_on_document_changed)
|
||||
file_manager.document_created.connect(_on_document_created)
|
||||
|
||||
func _on_validation_requested() -> void:
|
||||
# Reset and start the validation timer
|
||||
validation_timer.stop()
|
||||
validation_timer.start()
|
||||
|
||||
func _on_validation_timer_timeout() -> void:
|
||||
var document = file_manager.get_current_document()
|
||||
if document:
|
||||
validate_document(document)
|
||||
|
||||
func _on_document_changed(document: YAMLEditorDocument) -> void:
|
||||
# Show any existing validation results
|
||||
if document.validation_result:
|
||||
validation_completed.emit(document)
|
||||
|
||||
# Run validation if no results exist or document has errors
|
||||
if document.validation_result == null or document.has_error():
|
||||
validate_document(document)
|
||||
|
||||
func _on_document_created(document: YAMLEditorDocument) -> void:
|
||||
# Validate new document
|
||||
validate_document(document)
|
||||
|
||||
func validate_document(document: YAMLEditorDocument) -> void:
|
||||
if document == null:
|
||||
return
|
||||
|
||||
if _is_validating:
|
||||
# Add to validation queue
|
||||
if not _validation_queue.has(document):
|
||||
_validation_queue.append(document)
|
||||
return
|
||||
|
||||
_is_validating = true
|
||||
|
||||
if _thread and _thread.is_started():
|
||||
_thread.wait_to_finish()
|
||||
|
||||
_thread = Thread.new()
|
||||
_thread.start(_validation_thread_function.bind(document))
|
||||
|
||||
func _validation_thread_function(document: YAMLEditorDocument) -> void:
|
||||
# YAML validation is thread-safe
|
||||
var result = YAML.validate_syntax(document.content)
|
||||
|
||||
# Update document on main thread
|
||||
call_deferred("_finish_validation", document, result)
|
||||
|
||||
func _finish_validation(document: YAMLEditorDocument, result: YAMLResult) -> void:
|
||||
# Update document with validation result
|
||||
document.set_validation_result(result)
|
||||
|
||||
# Emit signal
|
||||
validation_completed.emit(document)
|
||||
|
||||
# Process any pending validations
|
||||
_is_validating = false
|
||||
|
||||
if not _validation_queue.is_empty():
|
||||
var next_document = _validation_queue.pop_front()
|
||||
validate_document(next_document)
|
||||
|
||||
func mark_error_in_editor(line: int, message: String) -> void:
|
||||
if is_instance_valid(code_editor):
|
||||
code_editor.mark_error_line(line, message)
|
||||
|
||||
func clear_errors_in_editor() -> void:
|
||||
if is_instance_valid(code_editor):
|
||||
code_editor.clear_error_indicators()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbtbggxu57ydy
|
||||
@@ -0,0 +1,350 @@
|
||||
@tool
|
||||
class_name YAMLEditor extends Control
|
||||
|
||||
# Components
|
||||
var file_manager: YAMLEditorDocumentManager
|
||||
var validator: YAMLEditorValidator
|
||||
var session_manager: YAMLEditorSessionManager
|
||||
|
||||
# File system singleton
|
||||
var file_system: YAMLFileSystem
|
||||
|
||||
# UI references
|
||||
@export var menu_bar: YAMLEditorMenuBar
|
||||
@export var file_list: YAMLEditorFileList
|
||||
@export var resizable_container: HSplitContainer
|
||||
@export var code_edit: YAMLCodeEditor
|
||||
@export var status_panel: YAMLEditorStatusBar
|
||||
@export var find_replace_panel: YAMLEditorFindReplaceBar
|
||||
|
||||
func _ready() -> void:
|
||||
# Get reference to file system singleton first
|
||||
file_system = YAMLFileSystem.get_singleton()
|
||||
|
||||
# Initialize components
|
||||
file_manager = YAMLEditorDocumentManager.new(resizable_container)
|
||||
add_child(file_manager)
|
||||
|
||||
validator = YAMLEditorValidator.new()
|
||||
add_child(validator)
|
||||
|
||||
session_manager = YAMLEditorSessionManager.new()
|
||||
add_child(session_manager)
|
||||
|
||||
# Wait for UI to be ready
|
||||
await get_tree().process_frame
|
||||
|
||||
# Set up components
|
||||
file_manager.setup(file_list, code_edit)
|
||||
validator.setup(code_edit, file_manager)
|
||||
session_manager.setup(file_manager, resizable_container)
|
||||
|
||||
# Connect menu signals for file operations
|
||||
menu_bar.new_file.connect(_on_new_button_pressed)
|
||||
menu_bar.open_file.connect(_on_open_button_pressed)
|
||||
menu_bar.save_requested.connect(_on_save_button_pressed)
|
||||
menu_bar.save_as_requested.connect(_on_save_as_button_pressed)
|
||||
menu_bar.close_requested.connect(_on_close_current_file)
|
||||
|
||||
# Connect menu signals for edit options
|
||||
menu_bar.undo_requested.connect(_on_undo_requested)
|
||||
menu_bar.redo_requested.connect(_on_redo_requested)
|
||||
menu_bar.cut_requested.connect(_on_cut_requested)
|
||||
menu_bar.copy_requested.connect(_on_copy_requested)
|
||||
menu_bar.paste_requested.connect(_on_paste_requested)
|
||||
menu_bar.select_all_requested.connect(_on_select_all_requested)
|
||||
|
||||
# Connect menu signals for search
|
||||
menu_bar.find_requested.connect(_on_find_requested)
|
||||
menu_bar.find_next_requested.connect(_on_find_next_requested)
|
||||
menu_bar.find_previous_requested.connect(_on_find_previous_requested)
|
||||
menu_bar.replace_requested.connect(_on_replace_requested)
|
||||
|
||||
# Connect code editor signals
|
||||
code_edit.content_changed.connect(_on_content_changed)
|
||||
code_edit.save_requested.connect(_on_save_button_pressed)
|
||||
code_edit.close_requested.connect(_on_close_current_file)
|
||||
code_edit.undo_requested.connect(_on_undo_requested)
|
||||
code_edit.redo_requested.connect(_on_redo_requested)
|
||||
code_edit.caret_changed.connect(_on_caret_changed)
|
||||
code_edit.zoom_changed.connect(_on_zoom_changed) # Connect to new zoom signal
|
||||
|
||||
# Connect file manager signals
|
||||
file_manager.document_changed.connect(_on_document_changed)
|
||||
|
||||
# Connect validation signals
|
||||
validator.validation_completed.connect(_on_validation_completed)
|
||||
|
||||
# Set initial zoom text
|
||||
_on_zoom_changed(code_edit.zoom_level)
|
||||
|
||||
# Setup the find and replace panels
|
||||
find_replace_panel.replace_performed.connect(_on_replace_performed)
|
||||
find_replace_panel.replace_all_performed.connect(_on_replace_all_performed)
|
||||
|
||||
# Load previous session
|
||||
session_manager.load_session()
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventKey and event.keycode == KEY_ESCAPE and event.pressed:
|
||||
if find_replace_panel.visible:
|
||||
find_replace_panel.hide_panel()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed:
|
||||
match event.get_keycode_with_modifiers():
|
||||
KEY_MASK_CTRL | KEY_F:
|
||||
_on_find_requested()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_MASK_CTRL | KEY_R:
|
||||
_on_replace_requested()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_F3:
|
||||
_on_find_next_requested()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_MASK_SHIFT | KEY_F3:
|
||||
_on_find_previous_requested()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _on_zoom_changed(new_zoom_level: float) -> void:
|
||||
status_panel.set_zoom_level(new_zoom_level)
|
||||
|
||||
func _on_new_button_pressed() -> void:
|
||||
file_manager.new_file()
|
||||
|
||||
func _on_open_button_pressed() -> void:
|
||||
var file_dialog := EditorFileDialog.new()
|
||||
file_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE
|
||||
file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
|
||||
file_dialog.add_filter("*.yaml;YAML Files")
|
||||
file_dialog.add_filter("*.yml;YML Files")
|
||||
file_dialog.title = "Open YAML File"
|
||||
|
||||
file_dialog.file_selected.connect(
|
||||
func(path):
|
||||
file_manager.open_file(path)
|
||||
file_dialog.queue_free()
|
||||
)
|
||||
file_dialog.canceled.connect(func(): file_dialog.queue_free())
|
||||
|
||||
add_child(file_dialog)
|
||||
file_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _on_save_button_pressed() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
if document.is_untitled():
|
||||
_on_save_as_button_pressed()
|
||||
else:
|
||||
file_manager.save_document(document)
|
||||
|
||||
func _on_save_as_button_pressed() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
var file_dialog := EditorFileDialog.new()
|
||||
file_dialog.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE
|
||||
file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
|
||||
file_dialog.add_filter("*.yaml;YAML Files")
|
||||
file_dialog.add_filter("*.yml;YML Files")
|
||||
file_dialog.title = "Save YAML File As"
|
||||
|
||||
if not document.is_untitled():
|
||||
file_dialog.current_path = document.path
|
||||
|
||||
file_dialog.file_selected.connect(
|
||||
func(path):
|
||||
file_manager.save_document_as(document, path)
|
||||
file_dialog.queue_free()
|
||||
)
|
||||
file_dialog.canceled.connect(func(): file_dialog.queue_free())
|
||||
|
||||
add_child(file_dialog)
|
||||
file_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _on_content_changed() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
# Update document content
|
||||
file_manager.update_document_content(document, code_edit.text)
|
||||
|
||||
# Request validation
|
||||
validator.validate_document(document)
|
||||
|
||||
func _on_close_current_file() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if document:
|
||||
file_manager.close_document(document)
|
||||
|
||||
func _on_undo_requested() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
var state := document.undo()
|
||||
if not state:
|
||||
return
|
||||
|
||||
code_edit.set_text_and_preserve_state(document.content)
|
||||
|
||||
# Optionally restore caret position if needed
|
||||
if state.caret_line > 0 and state.caret_column > 0:
|
||||
if state.caret_line < code_edit.get_line_count():
|
||||
code_edit.set_caret_line(state.caret_line)
|
||||
if state.caret_column <= code_edit.get_line(state.caret_line).length():
|
||||
code_edit.set_caret_column(state.caret_column)
|
||||
|
||||
# Validate after undo
|
||||
validator.validate_document(document)
|
||||
|
||||
# And re-trigger search if we did undo
|
||||
if find_replace_panel.visible:
|
||||
find_replace_panel.trigger_search()
|
||||
|
||||
func _on_redo_requested() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
var state := document.redo()
|
||||
if not state:
|
||||
return
|
||||
|
||||
code_edit.set_text_and_preserve_state(document.content)
|
||||
|
||||
# Optionally restore caret position if needed
|
||||
if state.caret_line > 0 and state.caret_column > 0:
|
||||
if state.caret_line < code_edit.get_line_count():
|
||||
code_edit.set_caret_line(state.caret_line)
|
||||
if state.caret_column <= code_edit.get_line(state.caret_line).length():
|
||||
code_edit.set_caret_column(state.caret_column)
|
||||
|
||||
# Validate after redo
|
||||
validator.validate_document(document)
|
||||
|
||||
# And re-trigger search if we did redo
|
||||
if find_replace_panel.visible:
|
||||
find_replace_panel.trigger_search()
|
||||
|
||||
func _on_cut_requested() -> void:
|
||||
code_edit.cut_selection()
|
||||
|
||||
func _on_copy_requested() -> void:
|
||||
code_edit.copy_selection()
|
||||
|
||||
func _on_paste_requested() -> void:
|
||||
code_edit.paste_clipboard()
|
||||
|
||||
func _on_select_all_requested() -> void:
|
||||
code_edit.select_all()
|
||||
|
||||
# Search-related methods
|
||||
func _on_find_requested() -> void:
|
||||
find_replace_panel.show_find_panel()
|
||||
# Hide replace panel if we request just search
|
||||
if find_replace_panel.replace_panel_visible:
|
||||
find_replace_panel.replace_panel_visible = false
|
||||
|
||||
func _on_replace_requested() -> void:
|
||||
find_replace_panel.show_replace_panel()
|
||||
|
||||
func _on_find_next_requested() -> void:
|
||||
if find_replace_panel and find_replace_panel.visible:
|
||||
find_replace_panel.find_next()
|
||||
else:
|
||||
_on_find_requested()
|
||||
|
||||
func _on_find_previous_requested() -> void:
|
||||
if find_replace_panel and find_replace_panel.visible:
|
||||
find_replace_panel.find_previous()
|
||||
else:
|
||||
_on_find_requested()
|
||||
|
||||
func _on_replace_performed() -> void:
|
||||
# After a replace, request validation
|
||||
code_edit.validation_requested.emit()
|
||||
|
||||
# Update the current document
|
||||
_on_content_changed()
|
||||
|
||||
func _on_replace_all_performed() -> void:
|
||||
# After replace all, request validation
|
||||
code_edit.validation_requested.emit()
|
||||
|
||||
# Update the current document
|
||||
_on_content_changed()
|
||||
|
||||
# Show a message in the status bar
|
||||
if find_replace_panel.visible and find_replace_panel.replace_panel.visible:
|
||||
status_panel.set_status("Replacement complete", Color.GREEN)
|
||||
# Clear the status after a delay
|
||||
if get_tree():
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
status_panel.set_status("")
|
||||
|
||||
func _on_document_changed(document: YAMLEditorDocument) -> void:
|
||||
# Update status panel with document info
|
||||
_update_line_col_label()
|
||||
|
||||
# Show any validation errors
|
||||
if document.has_error():
|
||||
_display_validation_error(document)
|
||||
else:
|
||||
status_panel.set_status("")
|
||||
validator.clear_errors_in_editor()
|
||||
|
||||
# Re-trigger search
|
||||
if find_replace_panel.visible:
|
||||
find_replace_panel.trigger_search()
|
||||
|
||||
func _on_caret_changed() -> void:
|
||||
status_panel.set_line_column(code_edit.get_current_line_col_info())
|
||||
|
||||
func _on_validation_completed(document: YAMLEditorDocument) -> void:
|
||||
if document != file_manager.get_current_document():
|
||||
return
|
||||
|
||||
if document.has_error():
|
||||
_display_validation_error(document)
|
||||
else:
|
||||
status_panel.set_status("")
|
||||
validator.clear_errors_in_editor()
|
||||
|
||||
func _display_validation_error(document: YAMLEditorDocument) -> void:
|
||||
status_panel.set_validation_result(document.validation_result)
|
||||
|
||||
var result := document.validation_result
|
||||
if not result.has_error():
|
||||
return
|
||||
|
||||
# Mark error line in editor if possible
|
||||
var error := result.get_error_message()
|
||||
var line := result.get_error_line()
|
||||
if line >= 0:
|
||||
validator.mark_error_in_editor(line - 1, error) # Convert to 0-based line
|
||||
|
||||
func _has_unsaved_changes() -> bool:
|
||||
return file_manager.has_unsaved_changes()
|
||||
|
||||
func get_open_files() -> Array:
|
||||
return file_manager.get_open_paths()
|
||||
|
||||
func handle_filesystem_change() -> void:
|
||||
file_manager.handle_filesystem_change()
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||
# Save session when editor is closing
|
||||
session_manager.save_session()
|
||||
|
||||
func _update_line_col_label() -> void:
|
||||
# Allow one frame to pass to ensure the UI is updated
|
||||
if get_tree():
|
||||
await get_tree().process_frame
|
||||
_on_caret_changed()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bbbai2ba18l5f
|
||||
@@ -0,0 +1,400 @@
|
||||
[gd_scene load_steps=27 format=3 uid="uid://b7wsl0ss24hvb"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbbai2ba18l5f" path="res://addons/yaml/editor/yaml_editor.gd" id="1_jqpkv"]
|
||||
[ext_resource type="Script" uid="uid://c0q2685eddg35" path="res://addons/yaml/editor/syntax_highlighting/editor_syntax_highlighter.gd" id="2_0xmrl"]
|
||||
[ext_resource type="Script" uid="uid://drkui6da5o1ou" path="res://addons/yaml/editor/code_editor.gd" id="2_lgcdy"]
|
||||
[ext_resource type="Script" uid="uid://c15odacm31d03" path="res://addons/yaml/editor/file_list.gd" id="3_ufmqr"]
|
||||
[ext_resource type="Script" uid="uid://b35lyu1onhcoj" path="res://addons/yaml/editor/menu_bar.gd" id="5_bqqrn"]
|
||||
[ext_resource type="Script" uid="uid://bkanguis54ea3" path="res://addons/yaml/editor/status_bar.gd" id="6_rk03l"]
|
||||
[ext_resource type="Script" uid="uid://bep06otwjntx1" path="res://addons/yaml/editor/find_replace_bar.gd" id="7_vo7uj"]
|
||||
|
||||
[sub_resource type="Image" id="Image_cjchp"]
|
||||
data = {
|
||||
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 255, 92, 92, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 255, 93, 93, 255, 255, 92, 92, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 42, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 255, 93, 93, 41, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 45, 255, 93, 93, 44, 0, 0, 0, 0, 255, 91, 91, 42, 255, 91, 91, 42, 0, 0, 0, 0, 255, 91, 91, 45, 255, 93, 93, 44, 0, 0, 0, 0, 255, 91, 91, 42, 255, 91, 91, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_oh2kv"]
|
||||
image = SubResource("Image_cjchp")
|
||||
|
||||
[sub_resource type="EditorSyntaxHighlighter" id="EditorSyntaxHighlighter_68y40"]
|
||||
script = ExtResource("2_0xmrl")
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_k3025"]
|
||||
subpixel_positioning = 0
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_42q8o"]
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_k8p4n"]
|
||||
font_names = PackedStringArray("Monospace")
|
||||
subpixel_positioning = 0
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_wntl2"]
|
||||
fallbacks = Array[Font]([SubResource("SystemFont_k8p4n")])
|
||||
font_names = PackedStringArray("JetBrains Mono")
|
||||
subpixel_positioning = 0
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_i8tps"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_r4bqd"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_3ksor"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_33vym"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_jwbjn"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_xhoyn"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_vnqw7"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1i5or"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_vlfg7"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ki2vh"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ke1f7"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_v3fgn"]
|
||||
|
||||
[node name="YamlEditor" type="Control" node_paths=PackedStringArray("menu_bar", "file_list", "resizable_container", "code_edit", "status_panel", "find_replace_panel")]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
script = ExtResource("1_jqpkv")
|
||||
menu_bar = NodePath("MainMargin/MainVBox/MenuBarMargin/MenuBar")
|
||||
file_list = NodePath("MainMargin/MainVBox/ResizableContainer/FileList")
|
||||
resizable_container = NodePath("MainMargin/MainVBox/ResizableContainer")
|
||||
code_edit = NodePath("MainMargin/MainVBox/ResizableContainer/EditorPanel/YAMLCodeEdit")
|
||||
status_panel = NodePath("MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar")
|
||||
find_replace_panel = NodePath("MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar")
|
||||
|
||||
[node name="MainMargin" type="MarginContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 4
|
||||
theme_override_constants/margin_top = 1
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 4
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="MainVBox" type="VBoxContainer" parent="MainMargin"]
|
||||
custom_minimum_size = Vector2(200, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="MenuBarMargin" type="MarginContainer" parent="MainMargin/MainVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_bottom = 1
|
||||
|
||||
[node name="MenuBar" type="MenuBar" parent="MainMargin/MainVBox/MenuBarMargin" node_paths=PackedStringArray("file_menu", "edit_menu", "search_menu")]
|
||||
layout_mode = 2
|
||||
flat = true
|
||||
script = ExtResource("5_bqqrn")
|
||||
file_menu = NodePath("File")
|
||||
edit_menu = NodePath("Edit")
|
||||
search_menu = NodePath("Search")
|
||||
|
||||
[node name="File" type="PopupMenu" parent="MainMargin/MainVBox/MenuBarMargin/MenuBar"]
|
||||
title = "File"
|
||||
hide_on_checkable_item_selection = false
|
||||
item_count = 7
|
||||
item_0/text = "New"
|
||||
item_0/id = 0
|
||||
item_1/text = "Open..."
|
||||
item_1/id = 1
|
||||
item_2/id = -1
|
||||
item_2/separator = true
|
||||
item_3/text = "Save"
|
||||
item_3/id = 2
|
||||
item_4/text = "Save As..."
|
||||
item_4/id = 3
|
||||
item_5/id = -1
|
||||
item_5/separator = true
|
||||
item_6/text = "Close"
|
||||
item_6/id = 4
|
||||
|
||||
[node name="Edit" type="PopupMenu" parent="MainMargin/MainVBox/MenuBarMargin/MenuBar"]
|
||||
auto_translate_mode = 1
|
||||
item_count = 8
|
||||
item_0/text = "Undo"
|
||||
item_0/id = 0
|
||||
item_1/text = "Redo"
|
||||
item_1/id = 1
|
||||
item_2/id = -1
|
||||
item_2/separator = true
|
||||
item_3/text = "Cut"
|
||||
item_3/id = 2
|
||||
item_4/text = "Copy"
|
||||
item_4/id = 3
|
||||
item_5/text = "Paste"
|
||||
item_5/id = 4
|
||||
item_6/id = -1
|
||||
item_6/separator = true
|
||||
item_7/text = "Select All"
|
||||
item_7/id = 5
|
||||
|
||||
[node name="Search" type="PopupMenu" parent="MainMargin/MainVBox/MenuBarMargin/MenuBar"]
|
||||
auto_translate_mode = 1
|
||||
item_count = 5
|
||||
item_0/text = "Find..."
|
||||
item_0/id = 0
|
||||
item_1/text = "Find Next"
|
||||
item_1/id = 1
|
||||
item_2/text = "Find Previous"
|
||||
item_2/id = 2
|
||||
item_3/id = -1
|
||||
item_3/separator = true
|
||||
item_4/text = "Replace..."
|
||||
item_4/id = 3
|
||||
|
||||
[node name="ResizableContainer" type="HSplitContainer" parent="MainMargin/MainVBox"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
split_offset = 150
|
||||
|
||||
[node name="FileList" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer" node_paths=PackedStringArray("filter_input", "file_list")]
|
||||
custom_minimum_size = Vector2(150, 0)
|
||||
layout_mode = 2
|
||||
size_flags_stretch_ratio = 0.25
|
||||
script = ExtResource("3_ufmqr")
|
||||
filter_input = NodePath("FilterInput")
|
||||
file_list = NodePath("ItemList")
|
||||
|
||||
[node name="FilterInput" type="LineEdit" parent="MainMargin/MainVBox/ResizableContainer/FileList"]
|
||||
layout_mode = 2
|
||||
placeholder_text = "Filter Files"
|
||||
clear_button_enabled = true
|
||||
right_icon = SubResource("ImageTexture_oh2kv")
|
||||
|
||||
[node name="ItemList" type="ItemList" parent="MainMargin/MainVBox/ResizableContainer/FileList"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
allow_rmb_select = true
|
||||
auto_height = true
|
||||
|
||||
[node name="EditorPanel" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="YAMLCodeEdit" type="CodeEdit" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 14
|
||||
deselect_on_focus_loss_enabled = false
|
||||
scroll_smooth = true
|
||||
minimap_draw = true
|
||||
syntax_highlighter = SubResource("EditorSyntaxHighlighter_68y40")
|
||||
highlight_all_occurrences = true
|
||||
highlight_current_line = true
|
||||
draw_control_chars = true
|
||||
draw_tabs = true
|
||||
draw_spaces = true
|
||||
line_folding = true
|
||||
gutters_draw_breakpoints_gutter = true
|
||||
gutters_draw_bookmarks = true
|
||||
gutters_draw_executing_lines = true
|
||||
gutters_draw_line_numbers = true
|
||||
gutters_zero_pad_line_numbers = true
|
||||
gutters_draw_fold_gutter = true
|
||||
indent_size = 2
|
||||
indent_use_spaces = true
|
||||
indent_automatic = true
|
||||
indent_automatic_prefixes = Array[String]([":"])
|
||||
auto_brace_completion_enabled = true
|
||||
auto_brace_completion_highlight_matching = true
|
||||
script = ExtResource("2_lgcdy")
|
||||
|
||||
[node name="BottomToolbar" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="StatusBarMargin" type="MarginContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 0
|
||||
theme_override_constants/margin_top = 2
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 2
|
||||
|
||||
[node name="StatusBar" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin" node_paths=PackedStringArray("editor", "status_label", "zoom_button", "line_column_label")]
|
||||
layout_mode = 2
|
||||
script = ExtResource("6_rk03l")
|
||||
editor = NodePath("../../../YAMLCodeEdit")
|
||||
status_label = NodePath("StatusLabel")
|
||||
zoom_button = NodePath("ZoomButton")
|
||||
line_column_label = NodePath("LineColumnLabel")
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 2
|
||||
focus_mode = 2
|
||||
theme_override_fonts/font = SubResource("SystemFont_k3025")
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_42q8o")
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ZoomButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar"]
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = SubResource("SystemFont_wntl2")
|
||||
text = "100 %"
|
||||
flat = true
|
||||
|
||||
[node name="VSeparator" type="VSeparator" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="LineColumnLabel" type="Label" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
theme_override_fonts/font = SubResource("SystemFont_wntl2")
|
||||
text = "1 : 1"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="FindReplaceBar" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar" node_paths=PackedStringArray("find_input", "matches_label", "previous_button", "next_button", "match_case_checkbox", "whole_words_checkbox", "find_button_container", "find_options_container", "replace_input", "replace_button", "replace_all_button", "selection_only_checkbox", "replace_button_container", "replace_options_container", "hide_button", "editor", "vbox_container", "find_panel", "replace_panel")]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
script = ExtResource("7_vo7uj")
|
||||
find_input = NodePath("InputFieldContainer/FindInput")
|
||||
matches_label = NodePath("ButtonsContainer/MatchesContainer/MatchesLabel")
|
||||
previous_button = NodePath("ButtonsContainer/MatchesContainer/PreviousButton")
|
||||
next_button = NodePath("ButtonsContainer/MatchesContainer/NextButton")
|
||||
match_case_checkbox = NodePath("CheckBoxContainer/FindOptions/MatchCaseCheckbox")
|
||||
whole_words_checkbox = NodePath("CheckBoxContainer/FindOptions/WholeWordsCheckbox")
|
||||
find_button_container = NodePath("ButtonsContainer/MatchesContainer")
|
||||
find_options_container = NodePath("CheckBoxContainer/FindOptions")
|
||||
replace_input = NodePath("InputFieldContainer/ReplaceInput")
|
||||
replace_button = NodePath("ButtonsContainer/ReplaceButtonsContainer/ReplaceButton")
|
||||
replace_all_button = NodePath("ButtonsContainer/ReplaceButtonsContainer/ReplaceAllButton")
|
||||
selection_only_checkbox = NodePath("CheckBoxContainer/ReplaceOptions/SelectionOnlyCheckBox")
|
||||
replace_button_container = NodePath("ButtonsContainer/ReplaceButtonsContainer")
|
||||
replace_options_container = NodePath("CheckBoxContainer/ReplaceOptions")
|
||||
hide_button = NodePath("HideButton")
|
||||
editor = NodePath("../../YAMLCodeEdit")
|
||||
vbox_container = NodePath("CheckBoxContainer")
|
||||
find_panel = NodePath("CheckBoxContainer/FindOptions")
|
||||
replace_panel = NodePath("CheckBoxContainer/ReplaceOptions")
|
||||
|
||||
[node name="InputFieldContainer" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="FindInput" type="LineEdit" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/InputFieldContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
tooltip_text = "Find"
|
||||
placeholder_text = "Find"
|
||||
|
||||
[node name="ReplaceInput" type="LineEdit" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/InputFieldContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
placeholder_text = "Replace"
|
||||
|
||||
[node name="ButtonsContainer" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
|
||||
[node name="MatchesContainer" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="MatchesLabel" type="Label" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/MatchesContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 1
|
||||
theme_override_fonts/font = SubResource("SystemFont_k3025")
|
||||
text = "10 matches"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="PreviousButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/MatchesContainer"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Previous Match"
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_i8tps")
|
||||
theme_override_styles/disabled = SubResource("StyleBoxEmpty_r4bqd")
|
||||
theme_override_styles/hover_pressed = SubResource("StyleBoxEmpty_3ksor")
|
||||
theme_override_styles/hover = SubResource("StyleBoxEmpty_33vym")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxEmpty_jwbjn")
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_xhoyn")
|
||||
disabled = true
|
||||
icon = SubResource("ImageTexture_oh2kv")
|
||||
flat = true
|
||||
|
||||
[node name="NextButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/MatchesContainer"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Next Match"
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_vnqw7")
|
||||
theme_override_styles/disabled = SubResource("StyleBoxEmpty_1i5or")
|
||||
theme_override_styles/hover_pressed = SubResource("StyleBoxEmpty_vlfg7")
|
||||
theme_override_styles/hover = SubResource("StyleBoxEmpty_ki2vh")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxEmpty_ke1f7")
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_v3fgn")
|
||||
disabled = true
|
||||
icon = SubResource("ImageTexture_oh2kv")
|
||||
flat = true
|
||||
|
||||
[node name="ReplaceButtonsContainer" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ReplaceButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/ReplaceButtonsContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 4
|
||||
disabled = true
|
||||
text = "Replace"
|
||||
|
||||
[node name="ReplaceAllButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/ReplaceButtonsContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 4
|
||||
disabled = true
|
||||
text = "Replace All"
|
||||
|
||||
[node name="CheckBoxContainer" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="FindOptions" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 30)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="MatchCaseCheckbox" type="CheckBox" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer/FindOptions"]
|
||||
layout_mode = 2
|
||||
text = "Match Case"
|
||||
|
||||
[node name="WholeWordsCheckbox" type="CheckBox" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer/FindOptions"]
|
||||
layout_mode = 2
|
||||
text = "Whole Words"
|
||||
|
||||
[node name="ReplaceOptions" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 30)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="SelectionOnlyCheckBox" type="CheckBox" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer/ReplaceOptions"]
|
||||
layout_mode = 2
|
||||
text = "Selection Only
|
||||
"
|
||||
|
||||
[node name="HideButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Hide"
|
||||
icon = SubResource("ImageTexture_oh2kv")
|
||||
flat = true
|
||||
Reference in New Issue
Block a user