inline eval results below /= lines, remove status bar panel
This commit is contained in:
parent
1d3c03e23f
commit
f4b2173534
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use iced_wgpu::core::{
|
||||||
};
|
};
|
||||||
use iced_widget::container;
|
use iced_widget::container;
|
||||||
use iced_widget::markdown;
|
use iced_widget::markdown;
|
||||||
use iced_widget::text_editor::{self, Binding, KeyPress, Motion, Status, Style};
|
use iced_widget::text_editor::{self, 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};
|
||||||
|
|
||||||
|
|
@ -28,13 +28,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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,8 +86,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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +100,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,11 +132,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) {
|
||||||
|
|
@ -244,9 +281,8 @@ 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),
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(lang) = &self.lang {
|
|
||||||
let settings = SyntaxSettings {
|
let settings = SyntaxSettings {
|
||||||
lang: lang.clone(),
|
lang: self.lang.clone().unwrap_or_default(),
|
||||||
source: self.content.text(),
|
source: self.content.text(),
|
||||||
};
|
};
|
||||||
editor
|
editor
|
||||||
|
|
@ -258,14 +294,12 @@ impl EditorState {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
|
||||||
editor.into()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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(
|
||||||
|
|
@ -290,44 +324,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)
|
||||||
|
|
@ -336,6 +332,121 @@ impl EditorState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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('=')?;
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue