diff --git a/viewport/Cargo.toml b/viewport/Cargo.toml index 7ef741f..cc25583 100644 --- a/viewport/Cargo.toml +++ b/viewport/Cargo.toml @@ -16,6 +16,7 @@ wgpu = "27" raw-window-handle = "0.6" pollster = "0.4" smol_str = "0.2" +serde_json = "1" [build-dependencies] cbindgen = "0.27" diff --git a/viewport/include/acord.h b/viewport/include/acord.h index 3f4e2f2..083919c 100644 --- a/viewport/include/acord.h +++ b/viewport/include/acord.h @@ -13,6 +13,10 @@ #include #include +#define EVAL_RESULT_KIND 24 + +#define EVAL_ERROR_KIND 25 + typedef struct ViewportHandle ViewportHandle; struct ViewportHandle *viewport_create(void *nsview, float width, float height, float scale); diff --git a/viewport/src/editor.rs b/viewport/src/editor.rs index c8319a3..30cbb47 100644 --- a/viewport/src/editor.rs +++ b/viewport/src/editor.rs @@ -10,7 +10,7 @@ use iced_wgpu::core::{ use iced_widget::canvas; use iced_widget::container; use iced_widget::markdown; -use iced_widget::text_editor::{self, Action, Binding, KeyPress, Motion, Status, Style}; +use iced_widget::text_editor::{self, Action, Binding, Cursor, KeyPress, Motion, Position, Status, Style}; use iced_wgpu::core::text::highlighter::Format; use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings}; @@ -30,13 +30,14 @@ pub enum Message { ZoomReset, } +pub const RESULT_PREFIX: &str = "→ "; +pub const ERROR_PREFIX: &str = "⚠ "; + pub struct EditorState { pub content: text_editor::Content, pub font_size: f32, pub preview: bool, pub parsed: Vec, - pub eval_results: Vec<(usize, String)>, - pub eval_errors: Vec<(usize, String)>, pub lang: Option, scroll_offset: f32, } @@ -88,8 +89,6 @@ impl EditorState { font_size: 14.0, preview: false, parsed: Vec::new(), - eval_results: Vec::new(), - eval_errors: Vec::new(), lang: Some("rust".into()), scroll_offset: 0.0, } @@ -110,7 +109,7 @@ impl EditorState { } fn reparse(&mut self) { - let text = self.content.text(); + let text = self.get_clean_text(); self.parsed = markdown::parse(&text).collect(); } @@ -142,11 +141,49 @@ impl EditorState { self.reparse(); } + pub fn get_clean_text(&self) -> String { + strip_result_lines(&self.content.text()) + } + fn run_eval(&mut self) { - let text = self.content.text(); - let doc = crate::eval::evaluate_document(&text); - self.eval_results = doc.results.into_iter().map(|r| (r.line, r.result)).collect(); - self.eval_errors = doc.errors.into_iter().map(|e| (e.line, e.error)).collect(); + let old_cursor = self.content.cursor(); + let old_text = self.content.text(); + let clean_line = to_clean_line(&old_text, old_cursor.position.line); + let clean_col = old_cursor.position.column; + + let clean = strip_result_lines(&old_text); + let doc = crate::eval::evaluate_document(&clean); + + let mut insertions: Vec<(usize, Vec)> = Vec::new(); + for r in &doc.results { + let lines = format_result_lines(&r.result, &r.format); + if !lines.is_empty() { + insertions.push((r.line, lines)); + } + } + for e in &doc.errors { + insertions.push((e.line, vec![format!("{}{}", ERROR_PREFIX, e.error)])); + } + insertions.sort_by_key(|(line, _)| *line); + + let mut out_lines: Vec = clean.lines().map(|l| l.to_string()).collect(); + let mut offset = 0usize; + for (line, inject) in &insertions { + let insert_at = (*line + 1 + offset).min(out_lines.len()); + for (i, injected) in inject.iter().enumerate() { + out_lines.insert(insert_at + i, injected.clone()); + } + offset += inject.len(); + } + + let new_text = out_lines.join("\n"); + let new_line = from_clean_line(&new_text, clean_line); + + self.content = text_editor::Content::with_text(&new_text); + self.content.move_to(Cursor { + position: Position { line: new_line, column: clean_col }, + selection: None, + }); } pub fn update(&mut self, message: Message) { @@ -319,24 +356,19 @@ impl EditorState { selection: Color::from_rgba(0.3, 0.5, 0.8, 0.4), }); - let editor_el: Element<'_, Message, Theme, iced_wgpu::Renderer> = - if let Some(lang) = &self.lang { - let settings = SyntaxSettings { - lang: lang.clone(), - source: self.content.text(), - }; - editor - .highlight_with::( - settings, - |highlight, _theme| Format { - color: Some(syntax::highlight_color(highlight.kind)), - font: None, - }, - ) - .into() - } else { - editor.into() - }; + let settings = SyntaxSettings { + lang: self.lang.clone().unwrap_or_default(), + source: self.content.text(), + }; + let editor_el: Element<'_, Message, Theme, iced_wgpu::Renderer> = editor + .highlight_with::( + settings, + |highlight, _theme| Format { + color: Some(syntax::highlight_color(highlight.kind)), + font: None, + }, + ) + .into(); let gutter = Gutter { line_count: self.content.line_count(), @@ -360,7 +392,8 @@ impl EditorState { let mode_label = if self.preview { "Preview" } else { "Edit" }; let cursor = self.content.cursor(); - let line = cursor.position.line + 1; + let text = self.content.text(); + let line = to_clean_line(&text, cursor.position.line) + 1; let col = cursor.position.column + 1; let status_bar = iced_widget::container( @@ -385,44 +418,6 @@ impl EditorState { let mut col_items: Vec> = vec![main_content]; - if !self.eval_results.is_empty() || !self.eval_errors.is_empty() { - let mut result_items: Vec> = Vec::new(); - - for (ln, val) in &self.eval_results { - result_items.push( - iced_widget::text(format!("Ln {}: {}", ln + 1, val)) - .font(Font::MONOSPACE) - .size(11.0) - .color(Color::from_rgb(0.651, 0.890, 0.631)) - .into(), - ); - } - for (ln, err) in &self.eval_errors { - result_items.push( - iced_widget::text(format!("Ln {}: {}", ln + 1, err)) - .font(Font::MONOSPACE) - .size(11.0) - .color(Color::from_rgb(0.890, 0.400, 0.400)) - .into(), - ); - } - - let eval_panel = iced_widget::container( - iced_widget::column(result_items).spacing(2.0), - ) - .width(Length::Fill) - .padding(Padding { top: 4.0, right: 10.0, bottom: 4.0, left: 10.0 }) - .style(|_theme: &Theme| container::Style { - background: Some(Background::Color(Color::from_rgb(0.10, 0.10, 0.12))), - border: Border::default(), - text_color: None, - shadow: Shadow::default(), - snap: false, - }); - - col_items.push(eval_panel.into()); - } - col_items.push(status_bar.into()); iced_widget::column(col_items) @@ -509,6 +504,121 @@ impl canvas::Program for Gutter { } } +fn to_clean_line(text: &str, display_line: usize) -> usize { + let mut clean = 0; + for (i, line) in text.lines().enumerate() { + if i == display_line { + return clean; + } + if !is_result_line(line) { + clean += 1; + } + } + clean +} + +fn from_clean_line(text: &str, clean_target: usize) -> usize { + let mut clean = 0; + for (i, line) in text.lines().enumerate() { + if !is_result_line(line) { + if clean == clean_target { + return i; + } + clean += 1; + } + } + text.lines().count().saturating_sub(1) +} + +pub fn is_result_line(line: &str) -> bool { + let trimmed = line.trim_start(); + trimmed.starts_with(RESULT_PREFIX) || trimmed.starts_with(ERROR_PREFIX) +} + +fn strip_result_lines(text: &str) -> String { + let lines: Vec<&str> = text.lines().filter(|l| !is_result_line(l)).collect(); + let mut result = lines.join("\n"); + if text.ends_with('\n') { + result.push('\n'); + } + result +} + +fn format_result_lines(result: &str, format: &str) -> Vec { + match format { + "table" => format_table(result), + "tree" => format_tree(result), + _ => vec![format!("{}{}", RESULT_PREFIX, result)], + } +} + +fn format_table(json: &str) -> Vec { + let rows: Vec> = match serde_json::from_str(json) { + Ok(r) => r, + Err(_) => return vec![format!("{}{}", RESULT_PREFIX, json)], + }; + if rows.is_empty() { + return vec![format!("{}(empty table)", RESULT_PREFIX)]; + } + + let col_count = rows.iter().map(|r| r.len()).max().unwrap_or(0); + let mut widths = vec![0usize; col_count]; + for row in &rows { + for (i, cell) in row.iter().enumerate() { + if i < col_count { + widths[i] = widths[i].max(cell.len()); + } + } + } + + let mut lines = Vec::new(); + for (ri, row) in rows.iter().enumerate() { + let cells: Vec = (0..col_count) + .map(|i| { + let val = row.get(i).map(|s| s.as_str()).unwrap_or(""); + format!("{:width$}", val, width = widths[i]) + }) + .collect(); + lines.push(format!("{}│ {} │", RESULT_PREFIX, cells.join(" │ "))); + if ri == 0 && rows.len() > 1 { + let sep: Vec = widths.iter().map(|w| "─".repeat(*w)).collect(); + lines.push(format!("{}├─{}─┤", RESULT_PREFIX, sep.join("─┼─"))); + } + } + lines +} + +fn format_tree(json: &str) -> Vec { + let val: serde_json::Value = match serde_json::from_str(json) { + Ok(v) => v, + Err(_) => return vec![format!("{}{}", RESULT_PREFIX, json)], + }; + let mut lines = Vec::new(); + render_tree_node(&val, &mut lines, 0); + lines +} + +fn render_tree_node(val: &serde_json::Value, lines: &mut Vec, depth: usize) { + let indent = " ".repeat(depth); + match val { + serde_json::Value::Array(items) => { + for item in items { + render_tree_node(item, lines, depth + 1); + } + } + other => { + let display = match other { + serde_json::Value::String(s) => s.clone(), + serde_json::Value::Number(n) => n.to_string(), + serde_json::Value::Bool(b) => b.to_string(), + serde_json::Value::Null => "null".to_string(), + _ => other.to_string(), + }; + lines.push(format!("{}{}{}", RESULT_PREFIX, indent, display)); + } + } +} + fn parse_let_binding(line: &str) -> Option { let rest = line.strip_prefix("let ")?; let eq_pos = rest.find('=')?; diff --git a/viewport/src/lib.rs b/viewport/src/lib.rs index b8fea4b..7a7cbe4 100644 --- a/viewport/src/lib.rs +++ b/viewport/src/lib.rs @@ -163,7 +163,7 @@ pub extern "C" fn viewport_get_text(handle: *mut ViewportHandle) -> *mut c_char Some(h) => h, None => return std::ptr::null_mut(), }; - let text = h.state.content.text(); + let text = h.state.get_clean_text(); CString::new(text).unwrap_or_default().into_raw() } diff --git a/viewport/src/syntax.rs b/viewport/src/syntax.rs index 23cf0be..dd18345 100644 --- a/viewport/src/syntax.rs +++ b/viewport/src/syntax.rs @@ -3,6 +3,10 @@ use std::ops::Range; use iced_wgpu::core::text::highlighter; use iced_wgpu::core::Color; use acord_core::highlight::{highlight_source, HighlightSpan}; +use crate::editor::{RESULT_PREFIX, ERROR_PREFIX}; + +pub const EVAL_RESULT_KIND: u8 = 24; +pub const EVAL_ERROR_KIND: u8 = 25; #[derive(Clone, PartialEq)] pub struct SyntaxSettings { @@ -64,6 +68,14 @@ impl highlighter::Highlighter for SyntaxHighlighter { let ln = self.current_line; self.current_line += 1; + let trimmed = _line.trim_start(); + if trimmed.starts_with(RESULT_PREFIX) { + return vec![(0.._line.len(), SyntaxHighlight { kind: EVAL_RESULT_KIND })].into_iter(); + } + if trimmed.starts_with(ERROR_PREFIX) { + return vec![(0.._line.len(), SyntaxHighlight { kind: EVAL_ERROR_KIND })].into_iter(); + } + if ln >= self.line_offsets.len() { return Vec::new().into_iter(); } @@ -120,6 +132,8 @@ pub fn highlight_color(kind: u8) -> Color { 21 => Color::from_rgb(0.569, 0.878, 0.800), // label - teal 22 => Color::from_rgb(0.949, 0.604, 0.584), // escape - red 23 => Color::from_rgb(0.804, 0.839, 0.957), // embedded - text + 24 => Color::from_rgb(0.651, 0.890, 0.631), // eval result - green + 25 => Color::from_rgb(0.890, 0.400, 0.400), // eval error - muted red _ => Color::from_rgb(0.804, 0.839, 0.957), // default text } }