Files
parasitic-god/addons/limbo_console/command_history.gd

166 lines
3.6 KiB
GDScript

extends RefCounted
## Manages command history.
const HISTORY_FILE := "user://limbo_console_history.log"
var _entries: PackedStringArray
var _hist_idx = -1
var _iterators: Array[WrappingIterator]
var _is_dirty: bool = false
func push_entry(p_entry: String) -> void:
_push_entry(p_entry)
_reset_iterators()
func _push_entry(p_entry: String) -> void:
var idx: int = _entries.find(p_entry)
if idx != -1:
# Duplicate commands not allowed in history.
_entries.remove_at(idx)
_entries.append(p_entry)
_is_dirty = true
func get_entry(p_index: int) -> String:
return _entries[clampi(p_index, 0, _entries.size())]
func create_iterator() -> WrappingIterator:
var it := WrappingIterator.new(_entries)
_iterators.append(it)
return it
func release_iterator(p_iter: WrappingIterator) -> void:
_iterators.erase(p_iter)
func size() -> int:
return _entries.size()
func trim(p_max_size: int) -> void:
if _entries.size() > p_max_size:
_entries.slice(p_max_size - _entries.size())
_reset_iterators()
func clear() -> void:
_entries.clear()
func load(p_path: String = HISTORY_FILE) -> void:
var file := FileAccess.open(p_path, FileAccess.READ)
if not file:
return
while not file.eof_reached():
var line: String = file.get_line().strip_edges()
if not line.is_empty():
_push_entry(line)
file.close()
_reset_iterators()
_is_dirty = false
func save(p_path: String = HISTORY_FILE) -> void:
if not _is_dirty:
return
var file := FileAccess.open(p_path, FileAccess.WRITE)
if not file:
push_error("LimboConsole: Failed to save console history to file: ", p_path)
return
for line in _entries:
file.store_line(line)
file.close()
_is_dirty = false
## Searches history and returns an array starting with most relevant entries.
func fuzzy_match(p_query: String) -> PackedStringArray:
if len(p_query) == 0:
var copy := _entries.duplicate()
copy.reverse()
return copy
var results: Array = []
for entry: String in _entries:
var score: int = _compute_match_score(p_query.to_lower(), entry.to_lower())
if score > 0:
results.append({"entry": entry, "score": score})
results.sort_custom(func(a, b): return a.score > b.score)
return results.map(func(rec): return rec.entry)
func _reset_iterators() -> void:
for it in _iterators:
it._reassign(_entries)
## Scoring function for fuzzy matching.
static func _compute_match_score(query: String, target: String) -> int:
var score: int = 0
var query_index: int = 0
# Exact match. give unbeatable score
if query == target:
score = 99999
return score
for i in range(target.length()):
if query_index < query.length() and target[i] == query[query_index]:
score += 10 # Base score for a match
if i == 0 or target[i - 1] == " ": # Bonus for word start
score += 5
query_index += 1
if query_index == query.length():
break
# Ensure full query matches
return score if query_index == query.length() else 0
## Iterator that wraps around and resets on history change.
class WrappingIterator:
extends RefCounted
var _idx: int = -1
var _entries: PackedStringArray
func _init(p_entries: PackedStringArray) -> void:
_entries = p_entries
func prev() -> String:
_idx = wrapi(_idx - 1, -1, _entries.size())
if _idx == -1:
return String()
return _entries[_idx]
func next() -> String:
_idx = wrapi(_idx + 1, -1, _entries.size())
if _idx == -1:
return String()
return _entries[_idx]
func current() -> String:
if _idx < 0 or _idx >= _entries.size():
return String()
return _entries[_idx]
func reset() -> void:
_idx = -1
func _reassign(p_entries: PackedStringArray) -> void:
_idx = -1
_entries = p_entries