trace, console

This commit is contained in:
Face
2025-09-01 17:08:47 +03:00
parent f973bb55f6
commit a6e96328ae
34 changed files with 1353 additions and 65 deletions

View File

@@ -29,7 +29,7 @@ func _init():
timeout_manager = LuaTimeoutManager.new()
threaded_vm = ThreadedLuaVM.new()
threaded_vm.script_completed.connect(_on_threaded_script_completed)
threaded_vm.script_error.connect(func(e): print(e))
threaded_vm.script_error.connect(_on_threaded_script_error)
threaded_vm.dom_operation_request.connect(_handle_dom_operation)
threaded_vm.print_output.connect(_on_print_output)
@@ -645,8 +645,11 @@ func execute_lua_script(code: String):
func _on_threaded_script_completed(_result: Dictionary):
pass
func _on_print_output(message: String):
LuaPrintUtils.lua_print_direct(message)
func _on_threaded_script_error(error_message: String):
Trace.trace_error("RuntimeError: " + error_message)
func _on_print_output(message: Dictionary):
Trace.get_instance().log_message.emit(message, "lua", Time.get_ticks_msec() / 1000.0)
func kill_script_execution():
threaded_vm.stop_lua_thread()

View File

@@ -0,0 +1,574 @@
class_name DevToolsConsole
extends VBoxContainer
@onready var log_container: VBoxContainer = $ScrollContainer/LogContainer
@onready var input_line: CodeEdit = $InputContainer/InputLine
@onready var scroll_container: ScrollContainer = $ScrollContainer
@onready var clear_button: Button = $Toolbar/ClearButton
@onready var filter_input: LineEdit = $Toolbar/LineEdit
var log_entries: Array[Dictionary] = []
var current_filter: String = ""
var last_log_item: Control = null
var last_log_entry: Dictionary = {}
var input_history: Array[String] = []
var history_index: int = -1
func _ready():
connect_signals()
initialize_filter()
load_existing_logs()
visibility_changed.connect(_on_visibility_changed)
func initialize_filter() -> void:
filter_input.placeholder_text = "Filter"
current_filter = ""
func connect_signals() -> void:
Trace.get_instance().log_message.connect(_on_trace_message)
clear_button.pressed.connect(_on_clear_pressed)
input_line.gui_input.connect(_on_input_gui_input)
filter_input.text_changed.connect(_on_filter_changed)
func load_existing_logs() -> void:
var existing_messages = Trace.get_all_messages()
for msg in existing_messages:
add_log_entry(msg.message, msg.level, msg.timestamp)
func _on_trace_message(message: Variant, level: String, timestamp: float) -> void:
call_deferred("add_log_entry", message, level, timestamp)
func _on_lua_print(message: String) -> void:
add_log_entry(message, "lua", Time.get_ticks_msec() / 1000.0)
func add_log_entry(message: Variant, level: String, timestamp: float) -> void:
var entry = {
"message": message,
"level": level,
"timestamp": timestamp,
"count": 1
}
if can_group_with_last_entry(entry):
last_log_entry.count += 1
last_log_entry.timestamp = timestamp
log_entries[log_entries.size() - 1] = last_log_entry
update_log_item_display(last_log_item, last_log_entry)
return
log_entries.append(entry)
last_log_entry = entry
var should_add_separator = false
if level == "log" or level == "lua" or level == "input":
if log_container.get_child_count() > 0:
var last_displayed_entry = get_last_displayed_entry()
if last_displayed_entry and last_displayed_entry.level != "warning" and last_displayed_entry.level != "error":
should_add_separator = true
if should_add_separator:
var separator = HSeparator.new()
separator.add_theme_color_override("separator", Color.GRAY * 0.3)
log_container.add_child(separator)
var log_item = create_log_item(entry)
log_container.add_child(log_item)
last_log_item = log_item
apply_filter_to_item(log_item, entry)
call_deferred("_scroll_to_bottom")
func create_log_item(entry: Dictionary) -> Control:
if entry.level == "input":
var message_code_edit = CodeEdit.new()
var input_display_text = get_display_text_for_entry(entry)
message_code_edit.text = input_display_text
message_code_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
message_code_edit.scroll_fit_content_height = true
message_code_edit.editable = true
message_code_edit.context_menu_enabled = true
message_code_edit.shortcut_keys_enabled = true
message_code_edit.selecting_enabled = true
message_code_edit.deselect_on_focus_loss_enabled = true
message_code_edit.drag_and_drop_selection_enabled = false
message_code_edit.virtual_keyboard_enabled = false
message_code_edit.middle_mouse_paste_enabled = false
var code_style_normal = StyleBoxFlat.new()
code_style_normal.bg_color = Color.TRANSPARENT
code_style_normal.border_width_left = 0
code_style_normal.border_width_top = 0
code_style_normal.border_width_right = 0
code_style_normal.border_width_bottom = 0
code_style_normal.content_margin_bottom = 8
message_code_edit.add_theme_stylebox_override("normal", code_style_normal)
message_code_edit.add_theme_stylebox_override("focus", code_style_normal)
message_code_edit.syntax_highlighter = input_line.syntax_highlighter.duplicate()
message_code_edit.gui_input.connect(_on_log_code_edit_gui_input)
message_code_edit.focus_entered.connect(_on_log_code_edit_focus_entered.bind(message_code_edit))
message_code_edit.text_changed.connect(_on_log_code_edit_text_changed.bind(message_code_edit, input_display_text))
return message_code_edit
if entry.level == "lua" and entry.message is Dictionary and entry.message.has("parts"):
return create_structured_log_item(entry)
var panel = PanelContainer.new()
var style_box = StyleBoxFlat.new()
match entry.level:
"warning":
style_box.bg_color = Color.YELLOW
style_box.bg_color.a = 0.2
style_box.corner_radius_top_left = 6
style_box.corner_radius_top_right = 6
style_box.corner_radius_bottom_left = 6
style_box.corner_radius_bottom_right = 6
style_box.content_margin_left = 8
style_box.content_margin_right = 8
style_box.content_margin_top = 4
style_box.content_margin_bottom = 4
"error":
style_box.bg_color = Color.RED
style_box.bg_color.a = 0.2
style_box.corner_radius_top_left = 6
style_box.corner_radius_top_right = 6
style_box.corner_radius_bottom_left = 6
style_box.corner_radius_bottom_right = 6
style_box.content_margin_left = 8
style_box.content_margin_right = 8
style_box.content_margin_top = 4
style_box.content_margin_bottom = 4
_:
style_box.bg_color = Color.TRANSPARENT
panel.add_theme_stylebox_override("panel", style_box)
var container: Control
if entry.level == "warning" or entry.level == "error":
var margin_container = MarginContainer.new()
margin_container.add_child(panel)
container = margin_container
else:
container = panel
var message_label = Label.new()
var display_text = get_display_text_for_entry(entry)
message_label.text = display_text
message_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
message_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
match entry.level:
"warning":
message_label.add_theme_color_override("font_color", Color.YELLOW)
"error":
message_label.add_theme_color_override("font_color", Color.WHITE)
_:
message_label.add_theme_color_override("font_color", Color.WHITE)
panel.add_child(message_label)
return container
func _scroll_to_bottom() -> void:
if scroll_container:
scroll_container.scroll_vertical = int(scroll_container.get_v_scroll_bar().max_value)
func _on_clear_pressed() -> void:
for child in log_container.get_children():
child.queue_free()
log_entries.clear()
last_log_item = null
last_log_entry = {}
Trace.clear_messages()
func _on_input_gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed:
if event.keycode == KEY_ENTER and event.ctrl_pressed:
var text = input_line.text.strip_edges()
if not text.is_empty():
_on_input_submitted(text)
input_line.text = ""
get_viewport().set_input_as_handled()
elif event.keycode == KEY_UP and not event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed:
if input_line.get_caret_column() == 0 and input_line.get_caret_line() == 0:
navigate_history_up()
get_viewport().set_input_as_handled()
elif event.keycode == KEY_DOWN and not event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed:
var caret_pos = input_line.get_caret_column()
var last_line = input_line.get_line_count() - 1
var last_line_length = input_line.get_line(last_line).length()
if input_line.get_caret_line() == last_line and caret_pos == last_line_length:
navigate_history_down()
get_viewport().set_input_as_handled()
func navigate_history_up() -> void:
if input_history.is_empty():
return
if history_index == -1:
history_index = input_history.size() - 1
elif history_index > 0:
history_index -= 1
if history_index >= 0 and history_index < input_history.size():
input_line.text = input_history[history_index]
call_deferred("move_caret_to_end")
func navigate_history_down() -> void:
if input_history.is_empty() or history_index == -1:
return
history_index += 1
if history_index >= input_history.size():
history_index = -1
input_line.text = ""
else:
input_line.text = input_history[history_index]
call_deferred("move_caret_to_end")
func move_caret_to_end() -> void:
var last_line = input_line.get_line_count() - 1
var last_line_length = input_line.get_line(last_line).length()
input_line.set_caret_line(last_line)
input_line.set_caret_column(last_line_length)
func _on_input_submitted(text: String) -> void:
if text.strip_edges().is_empty():
return
if input_history.is_empty() or input_history[input_history.size() - 1] != text:
input_history.append(text)
if input_history.size() > 100:
input_history.pop_front()
history_index = -1
add_log_entry("> " + text, "input", Time.get_ticks_msec() / 1000.0)
execute_lua_command(text)
func execute_lua_command(code: String) -> void:
var main_scene = Engine.get_main_loop().current_scene
if main_scene and main_scene.has_method("get_active_tab"):
var active_tab = main_scene.get_active_tab()
if active_tab and active_tab.lua_apis.size() > 0:
var lua_api = active_tab.lua_apis[0]
if lua_api:
var is_expression = is_likely_expression(code)
if is_expression:
var wrapped_code = "print(" + code + ")"
lua_api.execute_lua_script(wrapped_code)
else:
lua_api.execute_lua_script(code)
return
add_log_entry("No Lua context available", "error", Time.get_ticks_msec() / 1000.0)
func is_likely_expression(code: String) -> bool:
var trimmed = code.strip_edges()
var statement_keywords = ["if", "for", "while", "do", "function", "local", "return", "break"]
for keyword in statement_keywords:
if trimmed.begins_with(keyword + " ") or trimmed.begins_with(keyword + "("):
return false
if "=" in trimmed and not ("==" in trimmed or "!=" in trimmed or ">=" in trimmed or "<=" in trimmed):
return false
if "print(" in trimmed or "console.log(" in trimmed or "_trace_" in trimmed:
return false
if trimmed.ends_with(")") or trimmed.ends_with("]") or not (" " in trimmed):
return true
return true
func _on_filter_changed(new_text: String) -> void:
current_filter = new_text.strip_edges()
refresh_log_display()
func refresh_log_display() -> void:
for i in range(log_container.get_child_count()):
var child = log_container.get_child(i)
if child is HSeparator:
child.visible = should_separator_be_visible(i)
else:
var entry_index = get_entry_index_for_child(i)
if entry_index >= 0 and entry_index < log_entries.size():
var entry = log_entries[entry_index]
apply_filter_to_item(child, entry)
call_deferred("_scroll_to_bottom")
func apply_filter_to_item(item: Control, entry: Dictionary) -> void:
item.visible = entry_matches_filter(entry)
func entry_matches_filter(entry: Dictionary) -> bool:
if current_filter.is_empty():
return true
var message_text = ""
if entry.message is Dictionary and entry.message.has("parts"):
message_text = get_display_text_for_entry(entry)
else:
message_text = str(entry.message)
if current_filter.to_lower() in message_text.to_lower():
return true
if current_filter.to_lower() == entry.level.to_lower():
return true
return false
func get_last_displayed_entry() -> Dictionary:
for i in range(log_entries.size() - 2, -1, -1): # Start from second-to-last entry
var entry = log_entries[i]
if entry_matches_filter(entry):
return entry
return {}
func should_separator_be_visible(separator_index: int) -> bool:
var before_visible = false
var after_visible = false
if separator_index > 0:
var before_child = log_container.get_child(separator_index - 1)
before_visible = before_child.visible
if separator_index < log_container.get_child_count() - 1:
var after_child = log_container.get_child(separator_index + 1)
after_visible = after_child.visible
return before_visible and after_visible
func get_entry_index_for_child(child_index: int) -> int:
var entry_index = 0
for i in range(child_index):
var child = log_container.get_child(i)
if not child is HSeparator:
entry_index += 1
return entry_index
func can_group_with_last_entry(entry: Dictionary) -> bool:
if last_log_entry.is_empty() or last_log_item == null:
return false
if entry.level != last_log_entry.level:
return false
var current_message_text = ""
var last_message_text = ""
if entry.message is Dictionary and entry.message.has("parts"):
current_message_text = get_display_text_for_entry(entry)
else:
current_message_text = str(entry.message)
if last_log_entry.message is Dictionary and last_log_entry.message.has("parts"):
last_message_text = get_display_text_for_entry(last_log_entry)
else:
last_message_text = str(last_log_entry.message)
if current_message_text != last_message_text:
return false
if entry.level == "input":
return false
return true
func update_log_item_display(log_item: Control, entry: Dictionary) -> void:
if entry.level == "input":
var code_edit = log_item as CodeEdit
if code_edit:
code_edit.text = get_display_text_for_entry(entry)
else:
var label = find_message_label_in_item(log_item)
if label:
label.text = get_display_text_for_entry(entry)
func find_message_code_edit_in_item(item: Control) -> CodeEdit:
if item is CodeEdit:
return item as CodeEdit
if item is MarginContainer:
var panel = item.get_child(0) as PanelContainer
if panel:
return panel.get_child(0) as CodeEdit
elif item is PanelContainer:
return item.get_child(0) as CodeEdit
return null
func find_message_label_in_item(item: Control) -> Label:
if item is MarginContainer:
var panel = item.get_child(0) as PanelContainer
if panel:
return panel.get_child(0) as Label
elif item is PanelContainer:
return item.get_child(0) as Label
return null
func _on_log_code_edit_gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed:
var key = event.keycode
if event.ctrl_pressed and (key == KEY_C or key == KEY_A):
return
if key in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END, KEY_PAGEUP, KEY_PAGEDOWN]:
return
if event.shift_pressed and key in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END]:
return
get_viewport().set_input_as_handled()
elif event is InputEventMouseButton:
return
func _on_log_code_edit_focus_entered(code_edit: CodeEdit) -> void:
code_edit.release_focus()
func _on_log_code_edit_text_changed(code_edit: CodeEdit, original_text: String) -> void:
if code_edit.text != original_text:
code_edit.text = original_text
func get_display_text_for_entry(entry: Dictionary) -> String:
var count = entry.get("count", 1)
var message = entry.message
var base_text = ""
if message is Dictionary and message.has("parts"):
var parts = message.parts
var text_parts = []
for part in parts:
if part.type == "primitive":
text_parts.append(str(part.data))
elif part.type == "table":
var key_count = part.data.keys().size()
text_parts.append("Object {" + str(key_count) + "}")
base_text = "\t".join(text_parts)
else:
base_text = str(message)
if count > 1:
return "(" + str(count) + ") " + base_text
else:
return base_text
func create_structured_log_item(entry) -> Control:
var container = VBoxContainer.new()
var parts = entry.message.parts
if parts.size() == 1 and parts[0].type == "primitive":
var simple_panel = PanelContainer.new()
var style_box = StyleBoxFlat.new()
style_box.bg_color = Color.TRANSPARENT
style_box.content_margin_left = 10
style_box.content_margin_top = 5
style_box.content_margin_right = 10
style_box.content_margin_bottom = 5
simple_panel.add_theme_stylebox_override("panel", style_box)
var label = Label.new()
label.text = str(parts[0].data)
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
simple_panel.add_child(label)
container.add_child(simple_panel)
return container
for i in range(parts.size()):
var part = parts[i]
if part.type == "primitive":
var text_panel = PanelContainer.new()
var style_box = StyleBoxFlat.new()
style_box.bg_color = Color.TRANSPARENT
style_box.content_margin_left = 10
style_box.content_margin_top = 2
style_box.content_margin_right = 10
style_box.content_margin_bottom = 2
text_panel.add_theme_stylebox_override("panel", style_box)
var label = Label.new()
label.text = str(part.data)
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
text_panel.add_child(label)
container.add_child(text_panel)
elif part.type == "table":
var table_item = create_expandable_table_item(part.data)
container.add_child(table_item)
return container
func create_expandable_table_item(table_data) -> Control:
var main_container = VBoxContainer.new()
var header_container = HBoxContainer.new()
header_container.custom_minimum_size.y = 24
var chevron_button = Button.new()
chevron_button.text = ""
chevron_button.custom_minimum_size = Vector2(20, 20)
chevron_button.flat = true
chevron_button.focus_mode = Control.FOCUS_NONE
header_container.add_child(chevron_button)
var summary_label = Label.new()
var key_count = table_data.keys().size()
if table_data.has("__type"):
summary_label.text = str(table_data.get("__type", "Object")) + " {" + str(key_count) + "}"
else:
summary_label.text = "Object {" + str(key_count) + "}"
summary_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
summary_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header_container.add_child(summary_label)
main_container.add_child(header_container)
var content_container = VBoxContainer.new()
content_container.visible = false
for key in table_data.keys():
if key == "__type":
continue
var value = table_data[key]
var row_container = HBoxContainer.new()
var key_label = Label.new()
key_label.text = str(key) + ": "
key_label.custom_minimum_size.x = 80
key_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
row_container.add_child(key_label)
var value_label = Label.new()
if value is Dictionary:
var nested_count = value.keys().size()
value_label.text = "Object {" + str(nested_count) + "}"
value_label.modulate = Color.GRAY
elif value is Array:
value_label.text = "Array [" + str(value.size()) + "]"
value_label.modulate = Color.GRAY
else:
value_label.text = str(value)
value_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
value_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
value_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row_container.add_child(value_label)
content_container.add_child(row_container)
main_container.add_child(content_container)
chevron_button.pressed.connect(func():
content_container.visible = !content_container.visible
chevron_button.text = "" if content_container.visible else ""
)
return main_container
func _on_visibility_changed() -> void:
if visible:
input_line.grab_focus()

View File

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

View File

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

View File

@@ -0,0 +1,190 @@
@tool
class_name LuaSyntaxHighlighter
extends SyntaxHighlighter
@export_group("Colors")
@export var font_color: Color = Color("#d4d4d4", Color.WHITE)
@export var keyword_color: Color = Color.from_string("#c586c0", Color.WHITE)
@export var gurt_globals_color: Color = Color.from_string("#569cd6", Color.WHITE)
@export var function_color: Color = Color.from_string("#dcdcaa", Color.WHITE)
@export var member_color: Color = Color.from_string("#9cdcfe", Color.WHITE)
@export var number_color: Color = Color.from_string("#b5cea8", Color.WHITE)
@export var string_color: Color = Color.from_string("#ce9178", Color.WHITE)
@export var comment_color: Color = Color.from_string("#6a9955", Color.WHITE)
@export var symbol_color: Color = Color.from_string("#c586c0", Color.WHITE)
enum State { DEFAULT, IN_MULTILINE_COMMENT, IN_MULTILINE_STRING }
var _state_cache: Dictionary = {}
var _keywords: Dictionary = {}
var _built_in_functions: Dictionary = {}
var _gurt_globals: Dictionary = {}
var _member_keywords: Dictionary = {}
func _init() -> void:
for k in ["and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"]:
_keywords[k] = true
for f in ["assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "load", "loadfile", "next", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "tonumber", "tostring", "type", "xpcall"]:
_built_in_functions[f] = true
for g in ["gurt", "trace", "JSON", "Time", "WebSocket", "Clipboard", "Regex", "setTimeout", "setInterval", "clearTimeout", "clearInterval", "fetch", "urlEncode", "urlDecode"]:
_gurt_globals[g] = true
var members = [
"select", "selectAll", "create", "body", "location", "href", "reload", "goto", "query", "get", "has", "getAll",
"log", "warn", "error", "text", "value", "visible", "children", "parent", "nextSibling", "previousSibling",
"firstChild", "lastChild", "classList", "on", "append", "remove", "insertBefore", "insertAfter", "replace",
"clone", "getAttribute", "setAttribute", "show", "hide", "focus", "unfocus", "createTween", "unsubscribe",
"add", "remove", "toggle", "contains", "item", "length", "to", "duration", "easing", "transition", "play",
"pause", "stop", "currentTime", "volume", "loop", "src", "playing", "paused", "withContext", "fillRect",
"strokeRect", "clearRect", "drawCircle", "drawText", "setFont", "measureText", "beginPath", "moveTo",
"lineTo", "closePath", "stroke", "fill", "arc", "quadraticCurveTo", "bezierCurveTo", "setStrokeStyle",
"setFillStyle", "setLineWidth", "save", "translate", "rotate", "scale", "restore", "source", "ok", "json",
"status", "statusText", "headers", "stringify", "parse", "now", "format", "date", "sleep", "benchmark",
"timer", "delay", "elapsed", "reset", "complete", "remaining", "new", "send", "close", "readyState",
"test", "match", "tostring", "replace", "replaceAll", "trim",
]
for m in members:
_member_keywords[m] = true
func _is_whitespace(char: String) -> bool:
return char == " " or char == "\t"
func _clear_highlighting_cache():
_state_cache.clear()
func _get_initial_state() -> int:
return State.DEFAULT
func _get_line_state(p_line: int, p_state: int) -> int:
var current_state: int = p_state
var line_text: String = get_text_edit().get_line(p_line)
var line_len: int = line_text.length()
var i := 0
while i < line_len:
if current_state == State.DEFAULT:
if i + 3 < line_len and line_text.substr(i, 4) == "--[[":
current_state = State.IN_MULTILINE_COMMENT
i += 4
continue
if i + 1 < line_len and line_text.substr(i, 2) == "[[":
current_state = State.IN_MULTILINE_STRING
i += 2
continue
if line_text[i] == "'" or line_text[i] == "\"":
var quote = line_text[i]
i += 1
while i < line_len:
if line_text[i] == "\\": i += 2; continue
if line_text[i] == quote: break
i += 1
else:
var end_idx = line_text.find("]]", i)
if end_idx != -1:
current_state = State.DEFAULT
i = end_idx + 2
continue
else:
i = line_len
i += 1
_state_cache[p_line] = current_state
return current_state
func _get_line_syntax_highlighting(p_line: int) -> Dictionary:
var color_map := {}
var start_state: int = _state_cache.get(p_line - 1, _get_initial_state())
var line_text: String = get_text_edit().get_line(p_line)
var line_len: int = line_text.length()
color_map[0] = {"color": font_color}
var i := 0
if start_state != State.DEFAULT:
var end_idx = line_text.find("]]")
var region_color = comment_color if start_state == State.IN_MULTILINE_COMMENT else string_color
color_map[0] = {"color": region_color}
if end_idx == -1:
return color_map
else:
i = end_idx + 2
if i < line_len:
color_map[i] = {"color": font_color}
start_state = State.DEFAULT
while i < line_len:
var start_col: int = i
var current_char: String = line_text[i]
if current_char == "-" and i + 1 < line_len and line_text[i+1] == "-":
if not (i + 3 < line_len and line_text.substr(i, 4) == "--[["):
color_map[i] = {"color": comment_color}
return color_map
if current_char == "\"" or current_char == "'":
var quote = current_char
var string_start = i
i += 1
while i < line_len:
if line_text[i] == "\\": i += 2; continue
if line_text[i] == quote: i += 1; break
i += 1
var string_end = i
color_map[string_start] = {"color": string_color}
if string_end < line_len:
color_map[string_end] = {"color": font_color}
continue
if current_char.is_valid_int() or (current_char == "." and i + 1 < line_len and line_text[i+1].is_valid_int()):
var is_hex = false
if current_char == "0" and i + 1 < line_len and line_text[i+1].to_lower() == "x":
i += 2; is_hex = true
while i < line_len:
var char = line_text[i]
if (is_hex and char.is_valid_hex_number(false)) or \
(not is_hex and (char.is_valid_int() or char in "Ee.-+")):
i += 1
else:
break
var number_end = i
color_map[start_col] = {"color": number_color}
if number_end < line_len:
color_map[number_end] = {"color": font_color}
continue
if current_char.is_valid_identifier() and not current_char.is_valid_int():
while i < line_len and line_text[i].is_valid_identifier():
i += 1
var word = line_text.substr(start_col, i - start_col)
var color = font_color
if _keywords.has(word): color = keyword_color
elif _gurt_globals.has(word): color = gurt_globals_color
elif _built_in_functions.has(word): color = function_color
else:
var prev_char_idx = start_col - 1
while prev_char_idx >= 0 and _is_whitespace(line_text[prev_char_idx]):
prev_char_idx -= 1
var next_char_idx = i
while next_char_idx < line_len and _is_whitespace(line_text[next_char_idx]):
next_char_idx += 1
var is_member = prev_char_idx >= 0 and line_text[prev_char_idx] in [".", ":"]
var is_function_call = next_char_idx < line_len and line_text[next_char_idx] == "("
if is_member and _member_keywords.has(word): color = member_color
elif is_function_call: color = function_color
if color != font_color: color_map[start_col] = {"color": color}
continue
if not _is_whitespace(current_char):
color_map[i] = {"color": symbol_color}
if i + 1 < line_len:
color_map[i + 1] = {"color": font_color}
i += 1
return color_map

View File

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

View File

@@ -35,6 +35,9 @@ var loading_tween: Tween
var scroll_container: ScrollContainer = null
var website_container: VBoxContainer = null
var background_panel: PanelContainer = null
var main_hbox: HBoxContainer = null
var dev_tools: Control = null
var dev_tools_visible: bool = false
var lua_apis: Array[LuaAPI] = []
var current_url: String = ""
var has_content: bool = false
@@ -131,6 +134,9 @@ func _exit_tree():
background_panel.get_parent().remove_child(background_panel)
background_panel.queue_free()
if dev_tools and is_instance_valid(dev_tools):
dev_tools.queue_free()
remove_from_group("tabs")
func init_scene(parent_container: Control) -> void:
@@ -144,9 +150,15 @@ func init_scene(parent_container: Control) -> void:
style_box.bg_color = Color(1, 1, 1, 1) # White background
background_panel.add_theme_stylebox_override("panel", style_box)
main_hbox = HBoxContainer.new()
main_hbox.name = "Tab_MainHBox_" + str(get_instance_id())
main_hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll_container = ScrollContainer.new()
scroll_container.name = "Tab_ScrollContainer_" + str(get_instance_id())
scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Take 2/3 of space
website_container = VBoxContainer.new()
website_container.name = "Tab_WebsiteContainer_" + str(get_instance_id())
@@ -154,8 +166,15 @@ func init_scene(parent_container: Control) -> void:
website_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
website_container.add_theme_constant_override("separation", 22)
var dev_tools_scene = preload("res://Scenes/DevTools.tscn")
dev_tools = dev_tools_scene.instantiate()
dev_tools.name = "Tab_DevTools_" + str(get_instance_id())
dev_tools.visible = false
parent_container.call_deferred("add_child", background_panel)
background_panel.call_deferred("add_child", scroll_container)
background_panel.call_deferred("add_child", main_hbox)
main_hbox.call_deferred("add_child", scroll_container)
main_hbox.call_deferred("add_child", dev_tools)
scroll_container.call_deferred("add_child", website_container)
background_panel.visible = is_active
@@ -215,3 +234,21 @@ func _on_close_button_pressed() -> void:
await close_tween.finished
tab_closed.emit()
queue_free()
func toggle_dev_tools() -> void:
if not dev_tools:
return
dev_tools_visible = not dev_tools_visible
dev_tools.visible = dev_tools_visible
if dev_tools_visible:
scroll_container.size_flags_stretch_ratio = 2.0
dev_tools.size_flags_stretch_ratio = 1.0
else:
scroll_container.size_flags_stretch_ratio = 1.0
func get_dev_tools_console() -> DevToolsConsole:
if dev_tools and dev_tools.has_method("get_console"):
return dev_tools.get_console()
return null

View File

@@ -0,0 +1 @@
uid://8tv4fx7krev1

View File

@@ -272,7 +272,7 @@ static func render_new_element(element: HTMLParser.HTMLElement, parent_node: Nod
# Create the visual node for the element
var element_node = await main_scene.create_element_node(element, dom_parser)
if not element_node:
LuaPrintUtils.lua_print_direct("Failed to create visual node for element: " + str(element))
Trace.trace_log("Failed to create visual node for element: " + str(element))
return
# Set metadata so ul/ol can detect dynamically added li elements

View File

@@ -10,12 +10,9 @@ static func lua_print(vm: LuauVM) -> int:
message_parts.append(value_str)
var final_message = "\t".join(message_parts)
lua_print_direct(final_message)
Trace.trace_log(final_message)
return 0
static func lua_print_direct(msg) -> void:
print("GURT LOG: ", msg)
static func lua_value_to_string(vm: LuauVM, index: int) -> String:
var lua_type = vm.lua_type(index)

View File

@@ -3,7 +3,7 @@ extends RefCounted
signal script_completed(result: Dictionary)
signal script_error(error: String)
signal print_output(message: String)
signal print_output(print_data: Dictionary)
signal dom_operation_request(operation: Dictionary)
var lua_thread: Thread
@@ -238,23 +238,26 @@ func _print_handler(vm: LuauVM) -> int:
var num_args = vm.lua_gettop()
for i in range(1, num_args + 1):
var arg_str = ""
if vm.lua_isstring(i):
arg_str = vm.lua_tostring(i)
elif vm.lua_isnumber(i):
arg_str = str(vm.lua_tonumber(i))
elif vm.lua_isboolean(i):
arg_str = "true" if vm.lua_toboolean(i) else "false"
elif vm.lua_isnil(i):
arg_str = "nil"
var lua_type = vm.lua_type(i)
if lua_type == vm.LUA_TTABLE:
var table_data = vm.lua_todictionary(i)
message_parts.append({
"type": "table",
"data": table_data
})
else:
arg_str = vm.lua_typename(vm.lua_type(i))
message_parts.append(arg_str)
var value_str = LuaPrintUtils.lua_value_to_string(vm, i)
message_parts.append({
"type": "primitive",
"data": value_str
})
var final_message = "\t".join(message_parts)
var print_data = {
"parts": message_parts,
"count": message_parts.size()
}
call_deferred("_emit_print_output", final_message)
call_deferred("_emit_print_output", print_data)
return 0
@@ -267,10 +270,35 @@ func _time_sleep_handler(vm: LuauVM) -> int:
return 0
func _trace_log_handler(vm: LuauVM) -> int:
var message = vm.luaL_checkstring(1)
call_deferred("_emit_trace_message", message, "lua")
return 0
func _trace_warning_handler(vm: LuauVM) -> int:
var message = vm.luaL_checkstring(1)
call_deferred("_emit_trace_message", message, "warning")
return 0
func _trace_error_handler(vm: LuauVM) -> int:
var message = vm.luaL_checkstring(1)
call_deferred("_emit_trace_message", message, "error")
return 0
func _setup_threaded_gurt_api():
lua_vm.lua_pushcallable(_print_handler, "print")
lua_vm.lua_setglobal("print")
# Setup trace functions
lua_vm.lua_pushcallable(_trace_log_handler, "_trace_log")
lua_vm.lua_setglobal("_trace_log")
lua_vm.lua_pushcallable(_trace_warning_handler, "_trace_warning")
lua_vm.lua_setglobal("_trace_warning")
lua_vm.lua_pushcallable(_trace_error_handler, "_trace_error")
lua_vm.lua_setglobal("_trace_error")
LuaTimeUtils.setup_time_api(lua_vm)
lua_vm.lua_getglobal("Time")
@@ -281,9 +309,6 @@ func _setup_threaded_gurt_api():
lua_vm.lua_newtable()
lua_vm.lua_pushcallable(_print_handler, "gurt.log")
lua_vm.lua_setfield(-2, "log")
lua_vm.lua_pushcallable(_gurt_select_handler, "gurt.select")
lua_vm.lua_setfield(-2, "select")
@@ -357,6 +382,7 @@ func _setup_additional_lua_apis():
LuaCrumbsUtils.setup_crumbs_api(lua_vm)
LuaRegexUtils.setup_regex_api(lua_vm)
LuaURLUtils.setup_url_api(lua_vm)
Trace.setup_trace_api(lua_vm)
func _table_tostring_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
@@ -370,8 +396,8 @@ func _emit_script_completed(result: Dictionary):
func _emit_script_error(error: String):
script_error.emit(error)
func _emit_print_output(message: String):
print_output.emit(message)
func _emit_print_output(print_data: Dictionary):
print_output.emit(print_data)
func _gurt_select_all_handler(vm: LuauVM) -> int:
var selector: String = vm.luaL_checkstring(1)
@@ -567,3 +593,12 @@ func _create_threaded_interval(interval_id: int, delay_ms: int):
timeout_info.timer = timer
lua_api.add_child(timer)
timer.start()
func _emit_trace_message(message: String, level: String):
match level:
"lua", "log":
Trace.trace_log(message)
"warning":
Trace.trace_warning(message)
"error":
Trace.trace_error(message)

View File

@@ -0,0 +1,87 @@
class_name Trace
extends RefCounted
signal log_message(message: String, level: String, timestamp: float)
enum LogLevel {
LOG,
WARNING,
ERROR
}
static var _instance: Trace
static var _messages: Array[Dictionary] = []
static func get_instance() -> Trace:
if not _instance:
_instance = Trace.new()
return _instance
static func trace_log(message: String) -> void:
_emit_message(message, "log")
static func trace_warning(message: String) -> void:
_emit_message(message, "warning")
static func trace_error(message: String) -> void:
_emit_message(message, "error")
static func _emit_message(message: String, level: String) -> void:
var timestamp = Time.get_ticks_msec() / 1000.0
var log_entry = {
"message": message,
"level": level,
"timestamp": timestamp
}
_messages.append(log_entry)
get_instance().call_deferred("emit_signal", "log_message", message, level, timestamp)
match level:
"log":
print("TRACE LOG: ", message)
"warning":
print("TRACE WARNING: ", message)
"error":
print("TRACE ERROR: ", message)
static func get_all_messages() -> Array[Dictionary]:
return _messages.duplicate()
static func clear_messages() -> void:
_messages.clear()
static func _lua_trace_log_handler(vm: LuauVM) -> int:
var message = vm.luaL_checkstring(1)
vm.lua_getglobal("_trace_log")
vm.lua_pushstring(message)
vm.lua_call(1, 0)
return 0
static func _lua_trace_warn_handler(vm: LuauVM) -> int:
var message = vm.luaL_checkstring(1)
vm.lua_getglobal("_trace_warning")
vm.lua_pushstring(message)
vm.lua_call(1, 0)
return 0
static func _lua_trace_error_handler(vm: LuauVM) -> int:
var message = vm.luaL_checkstring(1)
vm.lua_getglobal("_trace_error")
vm.lua_pushstring(message)
vm.lua_call(1, 0)
return 0
static func setup_trace_api(vm: LuauVM) -> void:
vm.lua_newtable()
vm.lua_pushcallable(_lua_trace_log_handler, "trace.log")
vm.lua_setfield(-2, "log")
vm.lua_pushcallable(_lua_trace_warn_handler, "trace.warn")
vm.lua_setfield(-2, "warn")
vm.lua_pushcallable(_lua_trace_error_handler, "trace.error")
vm.lua_setfield(-2, "error")
vm.lua_setglobal("trace")

View File

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

View File

@@ -61,6 +61,16 @@ func _ready():
call_deferred("render")
func _input(event: InputEvent) -> void:
if Input.is_action_just_pressed("DevTools"):
_toggle_dev_tools()
get_viewport().set_input_as_handled()
func _toggle_dev_tools() -> void:
var active_tab = get_active_tab()
if active_tab:
active_tab.toggle_dev_tools()
func resolve_url(href: String) -> String:
return URLUtils.resolve_url(current_domain, href)
@@ -280,12 +290,16 @@ func render_content(html_bytes: PackedByteArray) -> void:
parser.register_dom_node(body, target_container)
var scripts = parser.find_all("script")
var lua_api = null
if scripts.size() > 0:
lua_api = LuaAPI.new()
add_child(lua_api)
if active_tab:
active_tab.lua_apis.append(lua_api)
var lua_api = LuaAPI.new()
add_child(lua_api)
if active_tab:
active_tab.lua_apis.append(lua_api)
lua_api.dom_parser = parser
if lua_api.threaded_vm:
lua_api.threaded_vm.dom_parser = parser
var i = 0
if body:
@@ -694,3 +708,9 @@ func get_active_website_container() -> Control:
if active_tab:
return active_tab.website_container
return website_container # fallback to original container
func get_dev_tools_console() -> DevToolsConsole:
var active_tab = get_active_tab()
if active_tab:
return active_tab.get_dev_tools_console()
return null