ced/src/app/state/processing.rs

283 lines
11 KiB
Rust
Raw Normal View History

2025-07-05 14:42:45 -04:00
use super::editor::{TextEditor, TextProcessingResult};
2025-07-15 00:42:01 -04:00
use eframe::egui;
2025-07-05 14:42:45 -04:00
impl TextEditor {
2025-07-15 00:42:01 -04:00
/// Process text content and find the longest line (only used for initial scan)
pub fn process_text_for_rendering(&mut self, content: &str, ui: &egui::Ui) {
let lines: Vec<&str> = content.lines().collect();
let line_count = lines.len().max(1);
if lines.is_empty() {
self.update_processing_result(TextProcessingResult {
line_count: 1,
longest_line_index: 0,
longest_line_length: 0,
longest_line_pixel_width: 0.0,
2025-07-05 14:42:45 -04:00
});
2025-07-15 00:42:01 -04:00
return;
2025-07-05 14:42:45 -04:00
}
2025-07-15 00:42:01 -04:00
// Find the longest line by character count first (fast)
let mut longest_line_index = 0;
let mut longest_line_length = 0;
for (index, line) in lines.iter().enumerate() {
let char_count = line.chars().count();
if char_count > longest_line_length {
longest_line_length = char_count;
longest_line_index = index;
}
}
2025-07-05 14:42:45 -04:00
2025-07-15 00:42:01 -04:00
// Calculate pixel width for the longest line
let font_id = self.get_font_id();
let longest_line_pixel_width = if longest_line_length > 0 {
let longest_line_text = lines[longest_line_index];
ui.fonts(|fonts| {
fonts.layout(
longest_line_text.to_string(),
font_id,
egui::Color32::WHITE,
f32::INFINITY,
).size().x
})
2025-07-05 14:42:45 -04:00
} else {
2025-07-15 00:42:01 -04:00
0.0
2025-07-05 14:42:45 -04:00
};
let result = TextProcessingResult {
line_count,
2025-07-15 00:42:01 -04:00
longest_line_index,
longest_line_length,
longest_line_pixel_width,
2025-07-05 14:42:45 -04:00
};
2025-07-15 00:42:01 -04:00
self.update_processing_result(result);
}
/// Efficiently detect and process line changes without full content iteration
pub fn process_incremental_change(&mut self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize, ui: &egui::Ui) {
// Calculate cursor line change incrementally
let line_change = self.calculate_cursor_line_change(old_content, new_content, old_cursor_pos, new_cursor_pos);
// Update current cursor line
self.current_cursor_line = (self.current_cursor_line as isize + line_change) as usize;
// Detect the type of change and handle appropriately
if old_content.len() == new_content.len() {
// Same length - likely a character replacement
self.handle_character_replacement(old_content, new_content, old_cursor_pos, new_cursor_pos, ui);
} else if new_content.len() > old_content.len() {
// Content added
self.handle_content_addition(old_content, new_content, old_cursor_pos, new_cursor_pos, ui);
} else {
// Content removed
self.handle_content_removal(old_content, new_content, old_cursor_pos, new_cursor_pos, ui);
2025-07-05 14:42:45 -04:00
}
2025-07-15 00:42:01 -04:00
self.previous_cursor_line = self.current_cursor_line;
2025-07-05 14:42:45 -04:00
}
2025-07-15 00:42:01 -04:00
/// Calculate the change in cursor line without full iteration
fn calculate_cursor_line_change(&self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize) -> isize {
// Count newlines up to the cursor position in both contents
let old_newlines = old_content[..old_cursor_pos.min(old_content.len())]
.bytes()
.filter(|&b| b == b'\n')
.count();
let new_newlines = new_content[..new_cursor_pos.min(new_content.len())]
.bytes()
.filter(|&b| b == b'\n')
.count();
new_newlines as isize - old_newlines as isize
}
/// Handle character replacement (same length change)
fn handle_character_replacement(&mut self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize, ui: &egui::Ui) {
// Extract the current line from new content
let current_line = self.extract_current_line(new_content, new_cursor_pos);
let current_line_length = current_line.chars().count();
self.update_line_if_longer(self.current_cursor_line, &current_line, current_line_length, ui);
}
/// Handle content addition
fn handle_content_addition(&mut self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize, ui: &egui::Ui) {
// Find the common prefix and suffix to identify the added text
let min_len = old_content.len().min(new_content.len());
let mut common_prefix = 0;
let mut common_suffix = 0;
// Find common prefix
for i in 0..min_len {
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
common_prefix += 1;
} else {
break;
}
}
// Find common suffix
for i in 0..min_len - common_prefix {
let old_idx = old_content.len() - 1 - i;
let new_idx = new_content.len() - 1 - i;
if old_content.as_bytes()[old_idx] == new_content.as_bytes()[new_idx] {
common_suffix += 1;
} else {
break;
}
}
// Extract the added text
let added_start = common_prefix;
let added_end = new_content.len() - common_suffix;
let added_text = &new_content[added_start..added_end];
let newlines_added = added_text.bytes().filter(|&b| b == b'\n').count();
if newlines_added > 0 {
// Lines were added, update line count
let mut current_result = self.get_text_processing_result();
current_result.line_count += newlines_added;
self.update_processing_result(current_result);
}
// Check if the current line is now longer
let current_line = self.extract_current_line(new_content, new_cursor_pos);
let current_line_length = current_line.chars().count();
self.update_line_if_longer(self.current_cursor_line, &current_line, current_line_length, ui);
}
/// Handle content removal
fn handle_content_removal(&mut self, old_content: &str, new_content: &str,
old_cursor_pos: usize, new_cursor_pos: usize, ui: &egui::Ui) {
// Find the common prefix and suffix to identify the removed text
let min_len = old_content.len().min(new_content.len());
let mut common_prefix = 0;
let mut common_suffix = 0;
// Find common prefix
for i in 0..min_len {
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
common_prefix += 1;
} else {
break;
}
}
// Find common suffix
for i in 0..min_len - common_prefix {
let old_idx = old_content.len() - 1 - i;
let new_idx = new_content.len() - 1 - i;
if old_content.as_bytes()[old_idx] == new_content.as_bytes()[new_idx] {
common_suffix += 1;
} else {
break;
}
}
// Extract the removed text
let removed_start = common_prefix;
let removed_end = old_content.len() - common_suffix;
let removed_text = &old_content[removed_start..removed_end];
let newlines_removed = removed_text.bytes().filter(|&b| b == b'\n').count();
if newlines_removed > 0 {
// Lines were removed, update line count
let mut current_result = self.get_text_processing_result();
current_result.line_count = current_result.line_count.saturating_sub(newlines_removed);
// If we removed the longest line, we need to rescan (but only if necessary)
if self.current_cursor_line <= current_result.longest_line_index {
// The longest line might have been affected, but let's be conservative
// and only rescan if we're sure it was the longest line
if self.current_cursor_line == current_result.longest_line_index {
self.process_text_for_rendering(new_content, ui);
return;
}
}
self.update_processing_result(current_result);
}
// Check if the current line changed
let current_line = self.extract_current_line(new_content, new_cursor_pos);
let current_line_length = current_line.chars().count();
// If this was the longest line and it got shorter, we might need to rescan
let current_result = self.get_text_processing_result();
if self.current_cursor_line == current_result.longest_line_index &&
current_line_length < current_result.longest_line_length {
self.process_text_for_rendering(new_content, ui);
} else {
self.update_line_if_longer(self.current_cursor_line, &current_line, current_line_length, ui);
}
}
/// Extract the current line efficiently without full content scan
fn extract_current_line(&self, content: &str, cursor_pos: usize) -> String {
let bytes = content.as_bytes();
// Find line start (search backwards from cursor)
let mut line_start = cursor_pos;
while line_start > 0 && bytes[line_start - 1] != b'\n' {
line_start -= 1;
}
// Find line end (search forwards from cursor)
let mut line_end = cursor_pos;
while line_end < bytes.len() && bytes[line_end] != b'\n' {
line_end += 1;
}
content[line_start..line_end].to_string()
}
/// Update longest line info if the current line is longer
fn update_line_if_longer(&mut self, line_index: usize, line_content: &str, line_length: usize, ui: &egui::Ui) {
let current_result = self.get_text_processing_result();
if line_length > current_result.longest_line_length {
let font_id = self.get_font_id();
let pixel_width = ui.fonts(|fonts| {
fonts.layout(
line_content.to_string(),
font_id,
egui::Color32::WHITE,
f32::INFINITY,
).size().x
});
let result = TextProcessingResult {
line_count: current_result.line_count,
longest_line_index: line_index,
longest_line_length: line_length,
longest_line_pixel_width: pixel_width,
};
self.update_processing_result(result);
}
}
/// Get the current text processing result
2025-07-05 14:42:45 -04:00
pub fn get_text_processing_result(&self) -> TextProcessingResult {
self.text_processing_result
.lock()
.map(|result| result.clone())
.unwrap_or_default()
}
2025-07-15 00:42:01 -04:00
/// Update the processing result atomically
fn update_processing_result(&self, result: TextProcessingResult) {
if let Ok(mut processing_result) = self.text_processing_result.lock() {
*processing_result = result;
}
}
2025-07-05 14:42:45 -04:00
}