230 lines
7.7 KiB
GDScript3
230 lines
7.7 KiB
GDScript3
|
@tool
|
||
|
extends Control
|
||
|
|
||
|
signal result_selected(path: String, cursor: Vector2, length: int)
|
||
|
|
||
|
|
||
|
const DialogueConstants = preload("../constants.gd")
|
||
|
|
||
|
|
||
|
@export var main_view: Control
|
||
|
@export var code_edit: CodeEdit
|
||
|
|
||
|
@onready var input: LineEdit = %Input
|
||
|
@onready var search_button: Button = %SearchButton
|
||
|
@onready var match_case_button: CheckBox = %MatchCaseButton
|
||
|
@onready var replace_toggle: CheckButton = %ReplaceToggle
|
||
|
@onready var replace_container: VBoxContainer = %ReplaceContainer
|
||
|
@onready var replace_input: LineEdit = %ReplaceInput
|
||
|
@onready var replace_selected_button: Button = %ReplaceSelectedButton
|
||
|
@onready var replace_all_button: Button = %ReplaceAllButton
|
||
|
@onready var results_container: VBoxContainer = %ResultsContainer
|
||
|
@onready var result_template: HBoxContainer = %ResultTemplate
|
||
|
|
||
|
var current_results: Dictionary = {}:
|
||
|
set(value):
|
||
|
current_results = value
|
||
|
update_results_view()
|
||
|
if current_results.size() == 0:
|
||
|
replace_selected_button.disabled = true
|
||
|
replace_all_button.disabled = true
|
||
|
else:
|
||
|
replace_selected_button.disabled = false
|
||
|
replace_all_button.disabled = false
|
||
|
get:
|
||
|
return current_results
|
||
|
|
||
|
var selections: PackedStringArray = []
|
||
|
|
||
|
|
||
|
func prepare() -> void:
|
||
|
input.grab_focus()
|
||
|
|
||
|
var template_label = result_template.get_node("Label")
|
||
|
template_label.get_theme_stylebox(&"focus").bg_color = code_edit.theme_overrides.current_line_color
|
||
|
template_label.add_theme_font_override(&"normal_font", code_edit.get_theme_font(&"font"))
|
||
|
|
||
|
replace_toggle.set_pressed_no_signal(false)
|
||
|
replace_container.hide()
|
||
|
|
||
|
$VBoxContainer/HBoxContainer/FindContainer/Label.text = DialogueConstants.translate(&"search.find")
|
||
|
input.placeholder_text = DialogueConstants.translate(&"search.placeholder")
|
||
|
input.text = ""
|
||
|
search_button.text = DialogueConstants.translate(&"search.find_all")
|
||
|
match_case_button.text = DialogueConstants.translate(&"search.match_case")
|
||
|
replace_toggle.text = DialogueConstants.translate(&"search.toggle_replace")
|
||
|
$VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with")
|
||
|
replace_input.placeholder_text = DialogueConstants.translate(&"search.replace_placeholder")
|
||
|
replace_input.text = ""
|
||
|
replace_all_button.text = DialogueConstants.translate(&"search.replace_all")
|
||
|
replace_selected_button.text = DialogueConstants.translate(&"search.replace_selected")
|
||
|
|
||
|
selections.clear()
|
||
|
self.current_results = {}
|
||
|
|
||
|
#region helpers
|
||
|
|
||
|
|
||
|
func update_results_view() -> void:
|
||
|
for child in results_container.get_children():
|
||
|
child.queue_free()
|
||
|
|
||
|
for path in current_results.keys():
|
||
|
var path_label: Label = Label.new()
|
||
|
path_label.text = path
|
||
|
# Show open files
|
||
|
if main_view.open_buffers.has(path):
|
||
|
path_label.text += "(*)"
|
||
|
results_container.add_child(path_label)
|
||
|
for path_result in current_results.get(path):
|
||
|
var result_item: HBoxContainer = result_template.duplicate()
|
||
|
|
||
|
var checkbox: CheckBox = result_item.get_node("CheckBox") as CheckBox
|
||
|
var key: String = get_selection_key(path, path_result)
|
||
|
checkbox.toggled.connect(func(is_pressed):
|
||
|
if is_pressed:
|
||
|
if not selections.has(key):
|
||
|
selections.append(key)
|
||
|
else:
|
||
|
if selections.has(key):
|
||
|
selections.remove_at(selections.find(key))
|
||
|
)
|
||
|
checkbox.set_pressed_no_signal(selections.has(key))
|
||
|
checkbox.visible = replace_toggle.button_pressed
|
||
|
|
||
|
var result_label: RichTextLabel = result_item.get_node("Label") as RichTextLabel
|
||
|
var colors: Dictionary = code_edit.theme_overrides
|
||
|
var highlight: String = ""
|
||
|
if replace_toggle.button_pressed:
|
||
|
var matched_word: String = "[bgcolor=" + colors.critical_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
|
||
|
highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]"
|
||
|
else:
|
||
|
highlight = "[bgcolor=" + colors.symbols_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
|
||
|
var text: String = path_result.text.substr(0, path_result.index) + highlight + path_result.text.substr(path_result.index + path_result.query.length())
|
||
|
result_label.text = "%s: %s" % [str(path_result.line).lpad(4), text]
|
||
|
result_label.gui_input.connect(func(event):
|
||
|
if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT and (event as InputEventMouseButton).double_click:
|
||
|
result_selected.emit(path, Vector2(path_result.index, path_result.line), path_result.query.length())
|
||
|
)
|
||
|
|
||
|
results_container.add_child(result_item)
|
||
|
|
||
|
|
||
|
func find_in_files() -> Dictionary:
|
||
|
var results: Dictionary = {}
|
||
|
|
||
|
var q: String = input.text
|
||
|
var cache = Engine.get_meta("DialogueCache")
|
||
|
var file: FileAccess
|
||
|
for path in cache.get_files():
|
||
|
var path_results: Array = []
|
||
|
var lines: PackedStringArray = []
|
||
|
|
||
|
if main_view.open_buffers.has(path):
|
||
|
lines = main_view.open_buffers.get(path).text.split("\n")
|
||
|
else:
|
||
|
file = FileAccess.open(path, FileAccess.READ)
|
||
|
lines = file.get_as_text().split("\n")
|
||
|
|
||
|
for i in range(0, lines.size()):
|
||
|
var index: int = find_in_line(lines[i], q)
|
||
|
while index > -1:
|
||
|
path_results.append({
|
||
|
line = i,
|
||
|
index = index,
|
||
|
text = lines[i],
|
||
|
matched_text = lines[i].substr(index, q.length()),
|
||
|
query = q
|
||
|
})
|
||
|
index = find_in_line(lines[i], q, index + q.length())
|
||
|
|
||
|
if file != null and file.is_open():
|
||
|
file.close()
|
||
|
|
||
|
if path_results.size() > 0:
|
||
|
results[path] = path_results
|
||
|
|
||
|
return results
|
||
|
|
||
|
|
||
|
func get_selection_key(path: String, path_result: Dictionary) -> String:
|
||
|
return "%s-%d-%d" % [path, path_result.line, path_result.index]
|
||
|
|
||
|
|
||
|
func find_in_line(line: String, query: String, from_index: int = 0) -> int:
|
||
|
if match_case_button.button_pressed:
|
||
|
return line.find(query, from_index)
|
||
|
else:
|
||
|
return line.findn(query, from_index)
|
||
|
|
||
|
|
||
|
func replace_results(only_selected: bool) -> void:
|
||
|
var file: FileAccess
|
||
|
var lines: PackedStringArray = []
|
||
|
for path in current_results:
|
||
|
if main_view.open_buffers.has(path):
|
||
|
lines = main_view.open_buffers.get(path).text.split("\n")
|
||
|
else:
|
||
|
file = FileAccess.open(path, FileAccess.READ_WRITE)
|
||
|
lines = file.get_as_text().split("\n")
|
||
|
|
||
|
# Read the results in reverse because we're going to be modifying them as we go
|
||
|
var path_results: Array = current_results.get(path).duplicate()
|
||
|
path_results.reverse()
|
||
|
for path_result in path_results:
|
||
|
var key: String = get_selection_key(path, path_result)
|
||
|
if not only_selected or (only_selected and selections.has(key)):
|
||
|
lines[path_result.line] = lines[path_result.line].substr(0, path_result.index) + replace_input.text + lines[path_result.line].substr(path_result.index + path_result.matched_text.length())
|
||
|
|
||
|
var replaced_text: String = "\n".join(lines)
|
||
|
if file != null and file.is_open():
|
||
|
file.seek(0)
|
||
|
file.store_string(replaced_text)
|
||
|
file.close()
|
||
|
else:
|
||
|
main_view.open_buffers.get(path).text = replaced_text
|
||
|
if main_view.current_file_path == path:
|
||
|
code_edit.text = replaced_text
|
||
|
|
||
|
current_results = find_in_files()
|
||
|
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region signals
|
||
|
|
||
|
|
||
|
func _on_search_button_pressed() -> void:
|
||
|
selections.clear()
|
||
|
self.current_results = find_in_files()
|
||
|
|
||
|
|
||
|
func _on_input_text_submitted(new_text: String) -> void:
|
||
|
_on_search_button_pressed()
|
||
|
|
||
|
|
||
|
func _on_replace_toggle_toggled(toggled_on: bool) -> void:
|
||
|
replace_container.visible = toggled_on
|
||
|
if toggled_on:
|
||
|
replace_input.grab_focus()
|
||
|
update_results_view()
|
||
|
|
||
|
|
||
|
func _on_replace_input_text_changed(new_text: String) -> void:
|
||
|
update_results_view()
|
||
|
|
||
|
|
||
|
func _on_replace_selected_button_pressed() -> void:
|
||
|
replace_results(true)
|
||
|
|
||
|
|
||
|
func _on_replace_all_button_pressed() -> void:
|
||
|
replace_results(false)
|
||
|
|
||
|
|
||
|
func _on_match_case_button_toggled(toggled_on: bool) -> void:
|
||
|
_on_search_button_pressed()
|
||
|
|
||
|
|
||
|
#endregion
|