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, ¤t_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, ¤t_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, ¤t_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
|
|
|
}
|