fix newline bug, debounce eval, skip result lines in gutter
This commit is contained in:
parent
01f34a4f34
commit
36895cd548
|
|
@ -1,4 +1,5 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use iced_wgpu::core::keyboard;
|
use iced_wgpu::core::keyboard;
|
||||||
use iced_wgpu::core::keyboard::key;
|
use iced_wgpu::core::keyboard::key;
|
||||||
|
|
@ -33,6 +34,8 @@ pub enum Message {
|
||||||
pub const RESULT_PREFIX: &str = "→ ";
|
pub const RESULT_PREFIX: &str = "→ ";
|
||||||
pub const ERROR_PREFIX: &str = "⚠ ";
|
pub const ERROR_PREFIX: &str = "⚠ ";
|
||||||
|
|
||||||
|
const EVAL_DEBOUNCE_MS: u128 = 300;
|
||||||
|
|
||||||
pub struct EditorState {
|
pub struct EditorState {
|
||||||
pub content: text_editor::Content<iced_wgpu::Renderer>,
|
pub content: text_editor::Content<iced_wgpu::Renderer>,
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
|
|
@ -40,6 +43,8 @@ pub struct EditorState {
|
||||||
pub parsed: Vec<markdown::Item>,
|
pub parsed: Vec<markdown::Item>,
|
||||||
pub lang: Option<String>,
|
pub lang: Option<String>,
|
||||||
scroll_offset: f32,
|
scroll_offset: f32,
|
||||||
|
eval_dirty: bool,
|
||||||
|
last_edit: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn md_style() -> markdown::Style {
|
fn md_style() -> markdown::Style {
|
||||||
|
|
@ -91,6 +96,8 @@ impl EditorState {
|
||||||
parsed: Vec::new(),
|
parsed: Vec::new(),
|
||||||
lang: Some("rust".into()),
|
lang: Some("rust".into()),
|
||||||
scroll_offset: 0.0,
|
scroll_offset: 0.0,
|
||||||
|
eval_dirty: false,
|
||||||
|
last_edit: Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +115,30 @@ impl EditorState {
|
||||||
self.lang = lang_from_extension(ext);
|
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) {
|
fn reparse(&mut self) {
|
||||||
let text = self.get_clean_text();
|
let text = self.get_clean_text();
|
||||||
self.parsed = markdown::parse(&text).collect();
|
self.parsed = markdown::parse(&text).collect();
|
||||||
|
|
@ -152,6 +183,19 @@ impl EditorState {
|
||||||
let clean_col = old_cursor.position.column;
|
let clean_col = old_cursor.position.column;
|
||||||
|
|
||||||
let clean = strip_result_lines(&old_text);
|
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 doc = crate::eval::evaluate_document(&clean);
|
||||||
|
|
||||||
let mut insertions: Vec<(usize, Vec<String>)> = Vec::new();
|
let mut insertions: Vec<(usize, Vec<String>)> = Vec::new();
|
||||||
|
|
@ -164,9 +208,25 @@ impl EditorState {
|
||||||
for e in &doc.errors {
|
for e in &doc.errors {
|
||||||
insertions.push((e.line, vec![format!("{}{}", ERROR_PREFIX, e.error)]));
|
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);
|
insertions.sort_by_key(|(line, _)| *line);
|
||||||
|
|
||||||
let mut out_lines: Vec<String> = clean.lines().map(|l| l.to_string()).collect();
|
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;
|
let mut offset = 0usize;
|
||||||
for (line, inject) in &insertions {
|
for (line, inject) in &insertions {
|
||||||
let insert_at = (*line + 1 + offset).min(out_lines.len());
|
let insert_at = (*line + 1 + offset).min(out_lines.len());
|
||||||
|
|
@ -190,6 +250,8 @@ impl EditorState {
|
||||||
match message {
|
match message {
|
||||||
Message::EditorAction(action) => {
|
Message::EditorAction(action) => {
|
||||||
let is_edit = action.is_edit();
|
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 {
|
if let Action::Scroll { lines } = &action {
|
||||||
let lh = self.line_height();
|
let lh = self.line_height();
|
||||||
|
|
@ -199,7 +261,11 @@ impl EditorState {
|
||||||
self.scroll_offset = self.scroll_offset.min(max.max(0.0));
|
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 cursor = self.content.cursor();
|
||||||
let line_text = self.content.line(cursor.position.line)
|
let line_text = self.content.line(cursor.position.line)
|
||||||
.map(|l| l.text.to_string())
|
.map(|l| l.text.to_string())
|
||||||
|
|
@ -254,11 +320,17 @@ impl EditorState {
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_edit {
|
if is_edit {
|
||||||
|
self.last_edit = Instant::now();
|
||||||
if self.lang.is_none() {
|
if self.lang.is_none() {
|
||||||
self.lang = detect_lang_from_content(&self.content.text());
|
self.lang = detect_lang_from_content(&self.content.text());
|
||||||
}
|
}
|
||||||
self.reparse();
|
self.reparse();
|
||||||
self.run_eval();
|
if is_enter || is_paste {
|
||||||
|
self.eval_dirty = false;
|
||||||
|
self.run_eval();
|
||||||
|
} else {
|
||||||
|
self.eval_dirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::InsertTable => {
|
Message::InsertTable => {
|
||||||
|
|
@ -370,12 +442,17 @@ impl EditorState {
|
||||||
)
|
)
|
||||||
.into();
|
.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 {
|
let gutter = Gutter {
|
||||||
line_count: self.content.line_count(),
|
line_count: self.content.line_count(),
|
||||||
|
source_line_count,
|
||||||
font_size: self.font_size,
|
font_size: self.font_size,
|
||||||
scroll_offset: self.scroll_offset,
|
scroll_offset: self.scroll_offset,
|
||||||
cursor_line: self.content.cursor().position.line,
|
cursor_line: self.content.cursor().position.line,
|
||||||
top_pad,
|
top_pad,
|
||||||
|
result_mask,
|
||||||
};
|
};
|
||||||
let gw = gutter.gutter_width();
|
let gw = gutter.gutter_width();
|
||||||
|
|
||||||
|
|
@ -428,19 +505,18 @@ impl EditorState {
|
||||||
|
|
||||||
struct Gutter {
|
struct Gutter {
|
||||||
line_count: usize,
|
line_count: usize,
|
||||||
|
source_line_count: usize,
|
||||||
font_size: f32,
|
font_size: f32,
|
||||||
scroll_offset: f32,
|
scroll_offset: f32,
|
||||||
cursor_line: usize,
|
cursor_line: usize,
|
||||||
top_pad: f32,
|
top_pad: f32,
|
||||||
|
result_mask: Vec<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gutter {
|
impl Gutter {
|
||||||
fn gutter_width(&self) -> f32 {
|
fn gutter_width(&self) -> f32 {
|
||||||
let digits = if self.line_count == 0 {
|
let count = if self.source_line_count == 0 { 1 } else { self.source_line_count };
|
||||||
1
|
let digits = (count as f32).log10().floor() as usize + 1;
|
||||||
} else {
|
|
||||||
(self.line_count as f32).log10().floor() as usize + 1
|
|
||||||
};
|
|
||||||
let char_width = self.font_size * 0.6;
|
let char_width = self.font_size * 0.6;
|
||||||
(digits.max(2) as f32 * char_width + 16.0).ceil()
|
(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 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 {
|
for i in 0..visible_count {
|
||||||
let line_idx = first_visible + i;
|
let line_idx = first_visible + i;
|
||||||
if line_idx >= self.line_count {
|
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;
|
let y = self.top_pad + i as f32 * lh - sub_pixel;
|
||||||
if y + lh < 0.0 || y > bounds.height {
|
if y + lh < 0.0 || y > bounds.height {
|
||||||
|
if line_idx < self.result_mask.len() && !self.result_mask[line_idx] {
|
||||||
|
source_num += 1;
|
||||||
|
}
|
||||||
continue;
|
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 {
|
let color = if line_idx == self.cursor_line {
|
||||||
Color::from_rgb(0.55, 0.55, 0.62)
|
Color::from_rgb(0.55, 0.55, 0.62)
|
||||||
} else {
|
} else {
|
||||||
Color::from_rgb(0.35, 0.35, 0.42)
|
Color::from_rgb(0.35, 0.35, 0.42)
|
||||||
};
|
};
|
||||||
frame.fill_text(canvas::Text {
|
frame.fill_text(canvas::Text {
|
||||||
content: format!("{}", line_idx + 1),
|
content: format!("{}", source_num),
|
||||||
position: Point::new(gw - 8.0, y),
|
position: Point::new(gw - 8.0, y),
|
||||||
max_width: gw,
|
max_width: gw,
|
||||||
color,
|
color,
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,8 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
handle.state.update(msg);
|
handle.state.update(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handle.state.tick();
|
||||||
|
|
||||||
let theme = Theme::Dark;
|
let theme = Theme::Dark;
|
||||||
let style = Style {
|
let style = Style {
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue