From 36895cd54864daf91f49b2a1b23f09832854c237 Mon Sep 17 00:00:00 2001 From: jess Date: Wed, 8 Apr 2026 03:16:50 -0700 Subject: [PATCH] fix newline bug, debounce eval, skip result lines in gutter --- viewport/src/editor.rs | 107 ++++++++++++++++++++++++++++++++++++++--- viewport/src/handle.rs | 2 + 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/viewport/src/editor.rs b/viewport/src/editor.rs index 30cbb47..dc3ce60 100644 --- a/viewport/src/editor.rs +++ b/viewport/src/editor.rs @@ -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, pub font_size: f32, @@ -40,6 +43,8 @@ pub struct EditorState { pub parsed: Vec, pub lang: Option, 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)> = 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 = 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 = 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, } 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 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 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, diff --git a/viewport/src/handle.rs b/viewport/src/handle.rs index 15778c5..f697945 100644 --- a/viewport/src/handle.rs +++ b/viewport/src/handle.rs @@ -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,