use crate::highlight::SyntaxStyleRegistry;
use crate::style::Style;
use crate::text::rope::RopeWrapper;
use crate::text::segment::{StyledChunk, StyledSegment};
use crate::unicode::WidthMethod;
use std::ops::Range;
use std::sync::Arc;
#[derive(Clone, Debug)]
struct MemEntry {
data: String,
owned: bool,
}
#[derive(Clone, Debug, Default)]
struct MemRegistry {
entries: Vec<Option<MemEntry>>,
free_list: Vec<u32>,
}
impl MemRegistry {
fn register(&mut self, data: &str, owned: bool) -> u32 {
if let Some(id) = self.free_list.pop() {
let idx = (id - 1) as usize;
self.entries[idx] = Some(MemEntry {
data: data.to_string(),
owned,
});
return id;
}
self.entries.push(Some(MemEntry {
data: data.to_string(),
owned,
}));
self.entries.len() as u32
}
fn replace(&mut self, id: u32, data: &str, owned: bool) {
if id == 0 {
return;
}
let idx = id.saturating_sub(1) as usize;
if let Some(slot) = self.entries.get_mut(idx) {
*slot = Some(MemEntry {
data: data.to_string(),
owned,
});
}
}
fn get(&self, id: u32) -> Option<&str> {
if id == 0 {
return None;
}
let idx = id.saturating_sub(1) as usize;
self.entries
.get(idx)
.and_then(|entry| entry.as_ref().map(|m| m.data.as_str()))
}
}
#[derive(Clone, Debug, Default)]
pub struct TextBuffer {
rope: RopeWrapper,
segments: Vec<StyledSegment>,
default_style: Style,
tab_width: u8,
mem_registry: MemRegistry,
width_method: WidthMethod,
syntax_styles: Option<Arc<SyntaxStyleRegistry>>,
revision: u64,
}
impl TextBuffer {
#[must_use]
pub fn new() -> Self {
Self {
rope: RopeWrapper::new(),
segments: Vec::new(),
default_style: Style::NONE,
tab_width: 4,
mem_registry: MemRegistry::default(),
width_method: WidthMethod::default(),
syntax_styles: None,
revision: 0,
}
}
#[must_use]
pub fn with_text(text: &str) -> Self {
Self {
rope: RopeWrapper::from_str(text),
segments: Vec::new(),
default_style: Style::NONE,
tab_width: 4,
mem_registry: MemRegistry::default(),
width_method: WidthMethod::default(),
syntax_styles: None,
revision: 0,
}
}
pub fn set_default_style(&mut self, style: Style) {
self.default_style = style;
}
#[must_use]
pub fn default_style(&self) -> Style {
self.default_style
}
pub fn set_tab_width(&mut self, width: u8) {
self.tab_width = width;
}
#[must_use]
pub fn tab_width(&self) -> u8 {
self.tab_width
}
pub fn set_width_method(&mut self, method: WidthMethod) {
self.width_method = method;
}
#[must_use]
pub fn width_method(&self) -> WidthMethod {
self.width_method
}
pub fn set_syntax_styles(&mut self, registry: Arc<SyntaxStyleRegistry>) {
self.syntax_styles = Some(registry);
}
pub fn clear_syntax_styles(&mut self) {
self.syntax_styles = None;
}
pub fn set_text(&mut self, text: &str) {
self.rope.replace(text);
self.segments.clear();
self.bump_revision();
}
pub fn append(&mut self, text: &str) {
self.rope.append(text);
self.bump_revision();
}
pub fn set_styled_text(&mut self, chunks: &[StyledChunk<'_>]) {
self.rope.clear();
self.segments.clear();
self.bump_revision();
let mut offset = 0;
for chunk in chunks {
let start = offset;
self.rope.append(chunk.text);
offset += chunk.text.len();
if !chunk.style.is_empty() {
self.segments
.push(StyledSegment::new(start..offset, chunk.style));
}
}
}
pub fn clear(&mut self) {
self.rope.clear();
self.segments.clear();
self.bump_revision();
}
pub fn reset(&mut self) {
self.clear();
}
#[must_use]
pub fn len_bytes(&self) -> usize {
self.rope.len_bytes()
}
#[must_use]
pub fn len_chars(&self) -> usize {
self.rope.len_chars()
}
#[must_use]
pub fn len_lines(&self) -> usize {
self.rope.len_lines()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rope.is_empty()
}
#[must_use]
pub fn line(&self, idx: usize) -> Option<String> {
self.rope.line(idx).map(|s| s.to_string())
}
pub fn lines(&self) -> impl Iterator<Item = String> + '_ {
self.rope.lines().map(|line| line.to_string())
}
#[must_use]
pub fn rope(&self) -> &RopeWrapper {
&self.rope
}
pub fn rope_mut(&mut self) -> &mut RopeWrapper {
self.bump_revision();
&mut self.rope
}
#[must_use]
pub fn revision(&self) -> u64 {
self.revision
}
pub fn add_highlight(&mut self, range: Range<usize>, style: Style, priority: u8) {
self.segments
.push(StyledSegment::new(range, style).with_priority(priority));
}
pub fn add_highlight_by_char_range(
&mut self,
char_start: usize,
char_end: usize,
style: Style,
priority: u8,
ref_id: Option<u16>,
) {
let start = self.rope.char_to_byte(char_start);
let end = self.rope.char_to_byte(char_end);
let mut segment = StyledSegment::new(start..end, style).with_priority(priority);
let id = ref_id.unwrap_or(0);
segment = segment.with_ref(id);
self.segments.push(segment);
}
pub fn add_highlight_line(
&mut self,
line: usize,
col_start: usize,
col_end: usize,
style: Style,
priority: u8,
ref_id: Option<u16>,
) {
let Some(line_slice) = self.rope.line(line) else {
return;
};
let line_len = line_slice.len_chars();
let safe_start = col_start.min(line_len);
let safe_end = col_end.min(line_len);
if safe_start >= safe_end {
return;
}
let line_start = self.rope.line_to_char(line);
let start = self.rope.char_to_byte(line_start + safe_start);
let end = self.rope.char_to_byte(line_start + safe_end);
let mut segment = StyledSegment::new(start..end, style)
.with_priority(priority)
.with_line(line);
let id = ref_id.unwrap_or(0);
segment = segment.with_ref(id);
self.segments.push(segment);
}
pub fn add_highlight_with_style_id(
&mut self,
line: usize,
col_start: usize,
col_end: usize,
style_id: u32,
priority: u8,
ref_id: Option<u16>,
) {
let Some(registry) = self.syntax_styles.as_ref() else {
return;
};
let Some(style) = registry.style(style_id) else {
return;
};
self.add_highlight_line(line, col_start, col_end, style, priority, ref_id);
}
pub fn clear_highlights(&mut self) {
self.segments
.retain(|seg| seg.ref_id.is_none() && seg.line.is_none());
}
pub fn remove_highlights_by_ref(&mut self, ref_id: u16) {
self.segments.retain(|seg| seg.ref_id != Some(ref_id));
}
pub fn clear_line_highlights(&mut self, line: usize) {
self.segments.retain(|seg| seg.line != Some(line));
}
pub fn clear_line_highlights_by_ref(&mut self, line: usize, ref_id: u16) {
self.segments
.retain(|seg| !(seg.line == Some(line) && seg.ref_id == Some(ref_id)));
}
pub fn register_text(&mut self, text: &str, owned: bool) -> u32 {
self.mem_registry.register(text, owned)
}
pub fn replace_text_by_id(&mut self, id: u32, text: &str, owned: bool) {
self.mem_registry.replace(id, text, owned);
}
pub fn set_text_from_mem_id(&mut self, id: u32) {
if let Some(text) = self.mem_registry.get(id).map(str::to_owned) {
self.set_text(&text);
}
}
pub fn segments_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &StyledSegment> {
self.segments
.iter()
.filter(move |seg| seg.range.start < range.end && range.start < seg.range.end)
}
#[must_use]
pub fn style_at(&self, pos: usize) -> Style {
let mut style = self.default_style;
let mut max_priority = 0u8;
for seg in &self.segments {
if seg.contains(pos) && seg.priority >= max_priority {
style = style.merge(seg.style);
max_priority = seg.priority;
}
}
style
}
#[must_use]
pub fn to_string(&self) -> String {
self.rope.to_string()
}
fn bump_revision(&mut self) {
self.revision = self.revision.wrapping_add(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Rgba;
#[test]
fn test_buffer_basic() {
let mut buffer = TextBuffer::new();
buffer.set_text("Hello, world!");
assert_eq!(buffer.len_chars(), 13);
}
#[test]
fn test_buffer_styled_text() {
let mut buffer = TextBuffer::new();
buffer.set_styled_text(&[
StyledChunk::new("Hello", Style::bold()),
StyledChunk::plain(", "),
StyledChunk::new("world", Style::fg(Rgba::RED)),
]);
assert_eq!(buffer.to_string(), "Hello, world");
}
#[test]
fn test_buffer_highlight() {
let mut buffer = TextBuffer::new();
buffer.set_text("Hello, world!");
buffer.add_highlight(0..5, Style::bold(), 0);
assert!(
buffer
.style_at(0)
.attributes
.contains(crate::style::TextAttributes::BOLD)
);
assert!(
!buffer
.style_at(6)
.attributes
.contains(crate::style::TextAttributes::BOLD)
);
}
#[test]
fn test_buffer_highlight_by_char_range_and_ref() {
let mut buffer = TextBuffer::new();
buffer.set_text("Hello, world!");
buffer.add_highlight_by_char_range(7, 12, Style::underline(), 1, Some(42));
assert!(
buffer
.style_at(buffer.rope().char_to_byte(8))
.attributes
.contains(crate::style::TextAttributes::UNDERLINE)
);
buffer.remove_highlights_by_ref(42);
assert!(
!buffer
.style_at(buffer.rope().char_to_byte(8))
.attributes
.contains(crate::style::TextAttributes::UNDERLINE)
);
}
#[test]
fn test_mem_registry_set_text() {
let mut buffer = TextBuffer::new();
let id = buffer.register_text("External", true);
buffer.set_text_from_mem_id(id);
assert_eq!(buffer.to_string(), "External");
}
#[test]
fn test_lines_iter() {
let buffer = TextBuffer::with_text("Line 1\nLine 2");
let lines: Vec<String> = buffer.lines().collect();
assert_eq!(lines, vec!["Line 1\n".to_string(), "Line 2".to_string()]);
}
}