use ratatui::style::Style;
use std::collections::HashMap;
use crate::model::marker::{MarkerId, MarkerList};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VirtualTextPosition {
BeforeChar,
AfterChar,
LineAbove,
LineBelow,
}
impl VirtualTextPosition {
pub fn is_line(&self) -> bool {
matches!(self, Self::LineAbove | Self::LineBelow)
}
pub fn is_inline(&self) -> bool {
matches!(self, Self::BeforeChar | Self::AfterChar)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct VirtualTextNamespace(pub String);
impl VirtualTextNamespace {
pub fn from_string(s: String) -> Self {
Self(s)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct VirtualText {
pub marker_id: MarkerId,
pub text: String,
pub style: Style,
pub position: VirtualTextPosition,
pub priority: i32,
pub string_id: Option<String>,
pub namespace: Option<VirtualTextNamespace>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VirtualTextId(pub u64);
pub struct VirtualTextManager {
texts: HashMap<VirtualTextId, VirtualText>,
next_id: u64,
}
impl VirtualTextManager {
pub fn new() -> Self {
Self {
texts: HashMap::new(),
next_id: 0,
}
}
pub fn add(
&mut self,
marker_list: &mut MarkerList,
position: usize,
text: String,
style: Style,
vtext_position: VirtualTextPosition,
priority: i32,
) -> VirtualTextId {
let marker_id = marker_list.create(position, false);
let id = VirtualTextId(self.next_id);
self.next_id += 1;
self.texts.insert(
id,
VirtualText {
marker_id,
text,
style,
position: vtext_position,
priority,
string_id: None,
namespace: None,
},
);
id
}
#[allow(clippy::too_many_arguments)]
pub fn add_with_id(
&mut self,
marker_list: &mut MarkerList,
position: usize,
text: String,
style: Style,
vtext_position: VirtualTextPosition,
priority: i32,
string_id: String,
) -> VirtualTextId {
let marker_id = marker_list.create(position, false);
let id = VirtualTextId(self.next_id);
self.next_id += 1;
self.texts.insert(
id,
VirtualText {
marker_id,
text,
style,
position: vtext_position,
priority,
string_id: Some(string_id),
namespace: None,
},
);
id
}
#[allow(clippy::too_many_arguments)]
pub fn add_line(
&mut self,
marker_list: &mut MarkerList,
position: usize,
text: String,
style: Style,
placement: VirtualTextPosition,
namespace: VirtualTextNamespace,
priority: i32,
) -> VirtualTextId {
debug_assert!(
placement.is_line(),
"add_line requires LineAbove or LineBelow"
);
let marker_id = marker_list.create(position, false);
let id = VirtualTextId(self.next_id);
self.next_id += 1;
self.texts.insert(
id,
VirtualText {
marker_id,
text,
style,
position: placement,
priority,
string_id: None,
namespace: Some(namespace),
},
);
id
}
pub fn remove_by_id(&mut self, marker_list: &mut MarkerList, string_id: &str) -> bool {
let to_remove: Vec<VirtualTextId> = self
.texts
.iter()
.filter_map(|(id, vtext)| {
if vtext.string_id.as_deref() == Some(string_id) {
Some(*id)
} else {
None
}
})
.collect();
let mut removed = false;
for id in to_remove {
if let Some(vtext) = self.texts.remove(&id) {
marker_list.delete(vtext.marker_id);
removed = true;
}
}
removed
}
pub fn remove_by_prefix(&mut self, marker_list: &mut MarkerList, prefix: &str) {
let markers_to_delete: Vec<(VirtualTextId, MarkerId)> = self
.texts
.iter()
.filter_map(|(id, vtext)| {
if let Some(ref sid) = vtext.string_id {
if sid.starts_with(prefix) {
return Some((*id, vtext.marker_id));
}
}
None
})
.collect();
for (id, marker_id) in markers_to_delete {
marker_list.delete(marker_id);
self.texts.remove(&id);
}
}
pub fn remove(&mut self, marker_list: &mut MarkerList, id: VirtualTextId) -> bool {
if let Some(vtext) = self.texts.remove(&id) {
marker_list.delete(vtext.marker_id);
true
} else {
false
}
}
pub fn clear(&mut self, marker_list: &mut MarkerList) {
for vtext in self.texts.values() {
marker_list.delete(vtext.marker_id);
}
self.texts.clear();
}
pub fn len(&self) -> usize {
self.texts.len()
}
pub fn is_empty(&self) -> bool {
self.texts.is_empty()
}
pub fn query_range(
&self,
marker_list: &MarkerList,
start: usize,
end: usize,
) -> Vec<(usize, &VirtualText)> {
let mut results: Vec<(usize, &VirtualText)> = self
.texts
.values()
.filter_map(|vtext| {
let pos = marker_list.get_position(vtext.marker_id)?;
if pos >= start && pos < end {
Some((pos, vtext))
} else {
None
}
})
.collect();
results.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.priority.cmp(&b.1.priority)));
results
}
pub fn build_lookup(
&self,
marker_list: &MarkerList,
start: usize,
end: usize,
) -> HashMap<usize, Vec<&VirtualText>> {
let mut lookup: HashMap<usize, Vec<&VirtualText>> = HashMap::new();
for vtext in self.texts.values() {
if let Some(pos) = marker_list.get_position(vtext.marker_id) {
if pos >= start && pos < end {
lookup.entry(pos).or_default().push(vtext);
}
}
}
for texts in lookup.values_mut() {
texts.sort_by_key(|vt| vt.priority);
}
lookup
}
pub fn clear_namespace(
&mut self,
marker_list: &mut MarkerList,
namespace: &VirtualTextNamespace,
) {
let to_remove: Vec<VirtualTextId> = self
.texts
.iter()
.filter_map(|(id, vtext)| {
if vtext.namespace.as_ref() == Some(namespace) {
Some(*id)
} else {
None
}
})
.collect();
for id in to_remove {
if let Some(vtext) = self.texts.remove(&id) {
marker_list.delete(vtext.marker_id);
}
}
}
pub fn query_lines_in_range(
&self,
marker_list: &MarkerList,
start: usize,
end: usize,
) -> Vec<(usize, &VirtualText)> {
let mut results: Vec<(usize, &VirtualText)> = self
.texts
.values()
.filter(|vtext| vtext.position.is_line())
.filter_map(|vtext| {
let pos = marker_list.get_position(vtext.marker_id)?;
if pos >= start && pos < end {
Some((pos, vtext))
} else {
None
}
})
.collect();
results.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.priority.cmp(&b.1.priority)));
results
}
pub fn query_inline_in_range(
&self,
marker_list: &MarkerList,
start: usize,
end: usize,
) -> Vec<(usize, &VirtualText)> {
let mut results: Vec<(usize, &VirtualText)> = self
.texts
.values()
.filter(|vtext| vtext.position.is_inline())
.filter_map(|vtext| {
let pos = marker_list.get_position(vtext.marker_id)?;
if pos >= start && pos < end {
Some((pos, vtext))
} else {
None
}
})
.collect();
results.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.priority.cmp(&b.1.priority)));
results
}
pub fn build_lines_lookup(
&self,
marker_list: &MarkerList,
start: usize,
end: usize,
) -> HashMap<usize, Vec<&VirtualText>> {
let mut lookup: HashMap<usize, Vec<&VirtualText>> = HashMap::new();
for vtext in self.texts.values() {
if !vtext.position.is_line() {
continue;
}
if let Some(pos) = marker_list.get_position(vtext.marker_id) {
if pos >= start && pos < end {
lookup.entry(pos).or_default().push(vtext);
}
}
}
for texts in lookup.values_mut() {
texts.sort_by_key(|vt| vt.priority);
}
lookup
}
}
impl Default for VirtualTextManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::style::Color;
fn hint_style() -> Style {
Style::default().fg(Color::DarkGray)
}
#[test]
fn test_new_manager() {
let manager = VirtualTextManager::new();
assert_eq!(manager.len(), 0);
assert!(manager.is_empty());
}
#[test]
fn test_add_virtual_text() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
let id = manager.add(
&mut marker_list,
10,
": i32".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
assert_eq!(manager.len(), 1);
assert!(!manager.is_empty());
assert_eq!(id.0, 0);
}
#[test]
fn test_remove_virtual_text() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
let id = manager.add(
&mut marker_list,
10,
": i32".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
assert_eq!(manager.len(), 1);
let removed = manager.remove(&mut marker_list, id);
assert!(removed);
assert_eq!(manager.len(), 0);
assert_eq!(marker_list.marker_count(), 0);
}
#[test]
fn test_remove_nonexistent() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
let removed = manager.remove(&mut marker_list, VirtualTextId(999));
assert!(!removed);
}
#[test]
fn test_clear() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
manager.add(
&mut marker_list,
10,
": i32".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
manager.add(
&mut marker_list,
20,
": String".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
assert_eq!(manager.len(), 2);
assert_eq!(marker_list.marker_count(), 2);
manager.clear(&mut marker_list);
assert_eq!(manager.len(), 0);
assert_eq!(marker_list.marker_count(), 0);
}
#[test]
fn test_query_range() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
manager.add(
&mut marker_list,
10,
": i32".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
manager.add(
&mut marker_list,
20,
": String".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
manager.add(
&mut marker_list,
30,
": bool".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
let results = manager.query_range(&marker_list, 15, 35);
assert_eq!(results.len(), 2);
assert_eq!(results[0].0, 20);
assert_eq!(results[0].1.text, ": String");
assert_eq!(results[1].0, 30);
assert_eq!(results[1].1.text, ": bool");
let results = manager.query_range(&marker_list, 0, 15);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0, 10);
assert_eq!(results[0].1.text, ": i32");
}
#[test]
fn test_query_empty_range() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
manager.add(
&mut marker_list,
10,
": i32".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
let results = manager.query_range(&marker_list, 100, 200);
assert!(results.is_empty());
}
#[test]
fn test_priority_ordering() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
manager.add(
&mut marker_list,
10,
"low".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
manager.add(
&mut marker_list,
10,
"high".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
10,
);
manager.add(
&mut marker_list,
10,
"medium".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
5,
);
let results = manager.query_range(&marker_list, 0, 20);
assert_eq!(results.len(), 3);
assert_eq!(results[0].1.text, "low");
assert_eq!(results[1].1.text, "medium");
assert_eq!(results[2].1.text, "high");
}
#[test]
fn test_build_lookup() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
manager.add(
&mut marker_list,
10,
": i32".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
manager.add(
&mut marker_list,
10,
" = 5".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
1,
);
manager.add(
&mut marker_list,
20,
": String".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
let lookup = manager.build_lookup(&marker_list, 0, 30);
assert_eq!(lookup.len(), 2);
let at_10 = lookup.get(&10).unwrap();
assert_eq!(at_10.len(), 2);
assert_eq!(at_10[0].text, ": i32"); assert_eq!(at_10[1].text, " = 5");
let at_20 = lookup.get(&20).unwrap();
assert_eq!(at_20.len(), 1);
assert_eq!(at_20[0].text, ": String");
}
#[test]
fn test_position_tracking_after_insert() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
manager.add(
&mut marker_list,
10,
": i32".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
marker_list.adjust_for_insert(5, 5);
let results = manager.query_range(&marker_list, 0, 20);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0, 15);
}
#[test]
fn test_position_tracking_after_delete() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
manager.add(
&mut marker_list,
20,
": i32".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
marker_list.adjust_for_delete(10, 5);
let results = manager.query_range(&marker_list, 0, 20);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0, 15);
}
#[test]
fn test_before_and_after_positions() {
let mut marker_list = MarkerList::new();
let mut manager = VirtualTextManager::new();
manager.add(
&mut marker_list,
10,
"/*param=*/".to_string(),
hint_style(),
VirtualTextPosition::BeforeChar,
0,
);
manager.add(
&mut marker_list,
10,
": Type".to_string(),
hint_style(),
VirtualTextPosition::AfterChar,
0,
);
let lookup = manager.build_lookup(&marker_list, 0, 20);
let at_10 = lookup.get(&10).unwrap();
assert_eq!(at_10.len(), 2);
let before = at_10
.iter()
.find(|vt| vt.position == VirtualTextPosition::BeforeChar);
let after = at_10
.iter()
.find(|vt| vt.position == VirtualTextPosition::AfterChar);
assert!(before.is_some());
assert!(after.is_some());
assert_eq!(before.unwrap().text, "/*param=*/");
assert_eq!(after.unwrap().text, ": Type");
}
}