merge rc1 (inline eval with table/tree formats) into features branch

This commit is contained in:
jess 2026-04-07 21:11:36 -07:00
commit cdd6ddd92d
5 changed files with 197 additions and 68 deletions

View File

@ -16,6 +16,7 @@ wgpu = "27"
raw-window-handle = "0.6" raw-window-handle = "0.6"
pollster = "0.4" pollster = "0.4"
smol_str = "0.2" smol_str = "0.2"
serde_json = "1"
[build-dependencies] [build-dependencies]
cbindgen = "0.27" cbindgen = "0.27"

View File

@ -13,6 +13,10 @@
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#define EVAL_RESULT_KIND 24
#define EVAL_ERROR_KIND 25
typedef struct ViewportHandle ViewportHandle; typedef struct ViewportHandle ViewportHandle;
struct ViewportHandle *viewport_create(void *nsview, float width, float height, float scale); struct ViewportHandle *viewport_create(void *nsview, float width, float height, float scale);

View File

@ -10,7 +10,7 @@ use iced_wgpu::core::{
use iced_widget::canvas; use iced_widget::canvas;
use iced_widget::container; use iced_widget::container;
use iced_widget::markdown; 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 iced_wgpu::core::text::highlighter::Format;
use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings}; use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings};
@ -30,13 +30,14 @@ pub enum Message {
ZoomReset, ZoomReset,
} }
pub const RESULT_PREFIX: &str = "";
pub const ERROR_PREFIX: &str = "";
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,
pub preview: bool, pub preview: bool,
pub parsed: Vec<markdown::Item>, pub parsed: Vec<markdown::Item>,
pub eval_results: Vec<(usize, String)>,
pub eval_errors: Vec<(usize, String)>,
pub lang: Option<String>, pub lang: Option<String>,
scroll_offset: f32, scroll_offset: f32,
} }
@ -88,8 +89,6 @@ impl EditorState {
font_size: 14.0, font_size: 14.0,
preview: false, preview: false,
parsed: Vec::new(), parsed: Vec::new(),
eval_results: Vec::new(),
eval_errors: Vec::new(),
lang: Some("rust".into()), lang: Some("rust".into()),
scroll_offset: 0.0, scroll_offset: 0.0,
} }
@ -110,7 +109,7 @@ impl EditorState {
} }
fn reparse(&mut self) { fn reparse(&mut self) {
let text = self.content.text(); let text = self.get_clean_text();
self.parsed = markdown::parse(&text).collect(); self.parsed = markdown::parse(&text).collect();
} }
@ -142,11 +141,49 @@ impl EditorState {
self.reparse(); self.reparse();
} }
pub fn get_clean_text(&self) -> String {
strip_result_lines(&self.content.text())
}
fn run_eval(&mut self) { fn run_eval(&mut self) {
let text = self.content.text(); let old_cursor = self.content.cursor();
let doc = crate::eval::evaluate_document(&text); let old_text = self.content.text();
self.eval_results = doc.results.into_iter().map(|r| (r.line, r.result)).collect(); let clean_line = to_clean_line(&old_text, old_cursor.position.line);
self.eval_errors = doc.errors.into_iter().map(|e| (e.line, e.error)).collect(); 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<String>)> = 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<String> = 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) { 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), selection: Color::from_rgba(0.3, 0.5, 0.8, 0.4),
}); });
let editor_el: Element<'_, Message, Theme, iced_wgpu::Renderer> = let settings = SyntaxSettings {
if let Some(lang) = &self.lang { lang: self.lang.clone().unwrap_or_default(),
let settings = SyntaxSettings { source: self.content.text(),
lang: lang.clone(), };
source: self.content.text(), let editor_el: Element<'_, Message, Theme, iced_wgpu::Renderer> = editor
}; .highlight_with::<SyntaxHighlighter>(
editor settings,
.highlight_with::<SyntaxHighlighter>( |highlight, _theme| Format {
settings, color: Some(syntax::highlight_color(highlight.kind)),
|highlight, _theme| Format { font: None,
color: Some(syntax::highlight_color(highlight.kind)), },
font: None, )
}, .into();
)
.into()
} else {
editor.into()
};
let gutter = Gutter { let gutter = Gutter {
line_count: self.content.line_count(), line_count: self.content.line_count(),
@ -360,7 +392,8 @@ impl EditorState {
let mode_label = if self.preview { "Preview" } else { "Edit" }; let mode_label = if self.preview { "Preview" } else { "Edit" };
let cursor = self.content.cursor(); 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 col = cursor.position.column + 1;
let status_bar = iced_widget::container( let status_bar = iced_widget::container(
@ -385,44 +418,6 @@ impl EditorState {
let mut col_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = let mut col_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> =
vec![main_content]; vec![main_content];
if !self.eval_results.is_empty() || !self.eval_errors.is_empty() {
let mut result_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = 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()); col_items.push(status_bar.into());
iced_widget::column(col_items) iced_widget::column(col_items)
@ -509,6 +504,121 @@ impl canvas::Program<Message, Theme, iced_wgpu::Renderer> 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<String> {
match format {
"table" => format_table(result),
"tree" => format_tree(result),
_ => vec![format!("{}{}", RESULT_PREFIX, result)],
}
}
fn format_table(json: &str) -> Vec<String> {
let rows: Vec<Vec<String>> = 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<String> = (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<String> = widths.iter().map(|w| "".repeat(*w)).collect();
lines.push(format!("{}├─{}─┤", RESULT_PREFIX, sep.join("─┼─")));
}
}
lines
}
fn format_tree(json: &str) -> Vec<String> {
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<String>, 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<String> { fn parse_let_binding(line: &str) -> Option<String> {
let rest = line.strip_prefix("let ")?; let rest = line.strip_prefix("let ")?;
let eq_pos = rest.find('=')?; let eq_pos = rest.find('=')?;

View File

@ -163,7 +163,7 @@ pub extern "C" fn viewport_get_text(handle: *mut ViewportHandle) -> *mut c_char
Some(h) => h, Some(h) => h,
None => return std::ptr::null_mut(), 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() CString::new(text).unwrap_or_default().into_raw()
} }

View File

@ -3,6 +3,10 @@ use std::ops::Range;
use iced_wgpu::core::text::highlighter; use iced_wgpu::core::text::highlighter;
use iced_wgpu::core::Color; use iced_wgpu::core::Color;
use acord_core::highlight::{highlight_source, HighlightSpan}; 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)] #[derive(Clone, PartialEq)]
pub struct SyntaxSettings { pub struct SyntaxSettings {
@ -64,6 +68,14 @@ impl highlighter::Highlighter for SyntaxHighlighter {
let ln = self.current_line; let ln = self.current_line;
self.current_line += 1; 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() { if ln >= self.line_offsets.len() {
return Vec::new().into_iter(); 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 21 => Color::from_rgb(0.569, 0.878, 0.800), // label - teal
22 => Color::from_rgb(0.949, 0.604, 0.584), // escape - red 22 => Color::from_rgb(0.949, 0.604, 0.584), // escape - red
23 => Color::from_rgb(0.804, 0.839, 0.957), // embedded - text 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 _ => Color::from_rgb(0.804, 0.839, 0.957), // default text
} }
} }