fix newline bug, debounce eval, skip result lines in gutter

This commit is contained in:
jess 2026-04-08 03:16:50 -07:00
parent 01f34a4f34
commit 36895cd548
2 changed files with 101 additions and 8 deletions

View File

@ -1,4 +1,5 @@
use std::sync::Arc;
use std::time::Instant;
use iced_wgpu::core::keyboard;
use iced_wgpu::core::keyboard::key;
@ -33,6 +34,8 @@ pub enum Message {
pub const RESULT_PREFIX: &str = "";
pub const ERROR_PREFIX: &str = "";
const EVAL_DEBOUNCE_MS: u128 = 300;
pub struct EditorState {
pub content: text_editor::Content<iced_wgpu::Renderer>,
pub font_size: f32,
@ -40,6 +43,8 @@ pub struct EditorState {
pub parsed: Vec<markdown::Item>,
pub lang: Option<String>,
scroll_offset: f32,
eval_dirty: bool,
last_edit: Instant,
}
fn md_style() -> markdown::Style {
@ -91,6 +96,8 @@ impl EditorState {
parsed: Vec::new(),
lang: Some("rust".into()),
scroll_offset: 0.0,
eval_dirty: false,
last_edit: Instant::now(),
}
}
@ -108,6 +115,30 @@ impl EditorState {
self.lang = lang_from_extension(ext);
}
pub fn tick(&mut self) {
if self.eval_dirty && self.last_edit.elapsed().as_millis() >= EVAL_DEBOUNCE_MS {
self.eval_dirty = false;
self.run_eval();
}
}
fn strip_results_in_place(&mut self) {
let text = self.content.text();
if !text.lines().any(|l| is_result_line(l)) {
return;
}
let cursor = self.content.cursor();
let clean_line = to_clean_line(&text, cursor.position.line);
let clean_col = cursor.position.column;
let clean = strip_result_lines(&text);
self.content = text_editor::Content::with_text(&clean);
let restored_line = from_clean_line(&clean, clean_line);
self.content.move_to(Cursor {
position: Position { line: restored_line, column: clean_col },
selection: None,
});
}
fn reparse(&mut self) {
let text = self.get_clean_text();
self.parsed = markdown::parse(&text).collect();
@ -152,6 +183,19 @@ impl EditorState {
let clean_col = old_cursor.position.column;
let clean = strip_result_lines(&old_text);
if !clean.lines().any(|l| l.trim_start().starts_with("/=")) {
if clean != old_text {
self.content = text_editor::Content::with_text(&clean);
let restored = from_clean_line(&clean, clean_line);
self.content.move_to(Cursor {
position: Position { line: restored, column: clean_col },
selection: None,
});
}
return;
}
let doc = crate::eval::evaluate_document(&clean);
let mut insertions: Vec<(usize, Vec<String>)> = Vec::new();
@ -164,9 +208,25 @@ impl EditorState {
for e in &doc.errors {
insertions.push((e.line, vec![format!("{}{}", ERROR_PREFIX, e.error)]));
}
if insertions.is_empty() {
if clean != old_text {
self.content = text_editor::Content::with_text(&clean);
let restored = from_clean_line(&clean, clean_line);
self.content.move_to(Cursor {
position: Position { line: restored, column: clean_col },
selection: None,
});
}
return;
}
insertions.sort_by_key(|(line, _)| *line);
let mut out_lines: Vec<String> = clean.lines().map(|l| l.to_string()).collect();
if clean.ends_with('\n') {
out_lines.push(String::new());
}
let mut offset = 0usize;
for (line, inject) in &insertions {
let insert_at = (*line + 1 + offset).min(out_lines.len());
@ -190,6 +250,8 @@ impl EditorState {
match message {
Message::EditorAction(action) => {
let is_edit = action.is_edit();
let is_enter = matches!(&action, Action::Edit(text_editor::Edit::Enter));
let is_paste = matches!(&action, Action::Edit(text_editor::Edit::Paste(_)));
if let Action::Scroll { lines } = &action {
let lh = self.line_height();
@ -199,7 +261,11 @@ impl EditorState {
self.scroll_offset = self.scroll_offset.min(max.max(0.0));
}
let auto_indent = if let text_editor::Action::Edit(text_editor::Edit::Enter) = &action {
if is_edit {
self.strip_results_in_place();
}
let auto_indent = if is_enter {
let cursor = self.content.cursor();
let line_text = self.content.line(cursor.position.line)
.map(|l| l.text.to_string())
@ -254,11 +320,17 @@ impl EditorState {
}
if is_edit {
self.last_edit = Instant::now();
if self.lang.is_none() {
self.lang = detect_lang_from_content(&self.content.text());
}
self.reparse();
self.run_eval();
if is_enter || is_paste {
self.eval_dirty = false;
self.run_eval();
} else {
self.eval_dirty = true;
}
}
}
Message::InsertTable => {
@ -370,12 +442,17 @@ impl EditorState {
)
.into();
let text = self.content.text();
let result_mask: Vec<bool> = text.lines().map(|l| is_result_line(l)).collect();
let source_line_count = result_mask.iter().filter(|r| !**r).count();
let gutter = Gutter {
line_count: self.content.line_count(),
source_line_count,
font_size: self.font_size,
scroll_offset: self.scroll_offset,
cursor_line: self.content.cursor().position.line,
top_pad,
result_mask,
};
let gw = gutter.gutter_width();
@ -428,19 +505,18 @@ impl EditorState {
struct Gutter {
line_count: usize,
source_line_count: usize,
font_size: f32,
scroll_offset: f32,
cursor_line: usize,
top_pad: f32,
result_mask: Vec<bool>,
}
impl Gutter {
fn gutter_width(&self) -> f32 {
let digits = if self.line_count == 0 {
1
} else {
(self.line_count as f32).log10().floor() as usize + 1
};
let count = if self.source_line_count == 0 { 1 } else { self.source_line_count };
let digits = (count as f32).log10().floor() as usize + 1;
let char_width = self.font_size * 0.6;
(digits.max(2) as f32 * char_width + 16.0).ceil()
}
@ -472,6 +548,13 @@ impl canvas::Program<Message, Theme, iced_wgpu::Renderer> for Gutter {
let gw = self.gutter_width();
let mut source_num = 0usize;
for idx in 0..first_visible {
if idx < self.result_mask.len() && !self.result_mask[idx] {
source_num += 1;
}
}
for i in 0..visible_count {
let line_idx = first_visible + i;
if line_idx >= self.line_count {
@ -479,15 +562,23 @@ impl canvas::Program<Message, Theme, iced_wgpu::Renderer> for Gutter {
}
let y = self.top_pad + i as f32 * lh - sub_pixel;
if y + lh < 0.0 || y > bounds.height {
if line_idx < self.result_mask.len() && !self.result_mask[line_idx] {
source_num += 1;
}
continue;
}
let is_result = line_idx < self.result_mask.len() && self.result_mask[line_idx];
if is_result {
continue;
}
source_num += 1;
let color = if line_idx == self.cursor_line {
Color::from_rgb(0.55, 0.55, 0.62)
} else {
Color::from_rgb(0.35, 0.35, 0.42)
};
frame.fill_text(canvas::Text {
content: format!("{}", line_idx + 1),
content: format!("{}", source_num),
position: Point::new(gw - 8.0, y),
max_width: gw,
color,

View File

@ -192,6 +192,8 @@ pub fn render(handle: &mut ViewportHandle) {
handle.state.update(msg);
}
handle.state.tick();
let theme = Theme::Dark;
let style = Style {
text_color: Color::WHITE,