Compare commits

..

No commits in common. "5b4abcf3e57da2c242ac47ec754ed49604940f60" and "01f34a4f34ca9fb7e7195551098c78d57acc4c8b" have entirely different histories.

14 changed files with 188 additions and 3146 deletions

View File

@ -6,6 +6,11 @@ import UniformTypeIdentifiers
extension Notification.Name { extension Notification.Name {
static let focusEditor = Notification.Name("focusEditor") static let focusEditor = Notification.Name("focusEditor")
static let focusTitle = Notification.Name("focusTitle") static let focusTitle = Notification.Name("focusTitle")
static let formatDocument = Notification.Name("formatDocument")
static let insertTable = Notification.Name("insertTable")
static let boldSelection = Notification.Name("boldSelection")
static let italicizeSelection = Notification.Name("italicizeSelection")
static let smartEval = Notification.Name("smartEval")
} }
class WindowController { class WindowController {
@ -58,7 +63,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
observeDocumentTitle() observeDocumentTitle()
observeDocumentText() observeDocumentText()
syncThemeToViewport()
DocumentBrowserController.shared = DocumentBrowserController(appState: appState) DocumentBrowserController.shared = DocumentBrowserController(appState: appState)
@ -277,19 +281,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
@objc private func boldSelection() { @objc private func boldSelection() {
viewport?.sendCommand(1) NotificationCenter.default.post(name: .boldSelection, object: nil)
} }
@objc private func italicizeSelection() { @objc private func italicizeSelection() {
viewport?.sendCommand(2) NotificationCenter.default.post(name: .italicizeSelection, object: nil)
} }
@objc private func insertTable() { @objc private func insertTable() {
viewport?.sendCommand(3) NotificationCenter.default.post(name: .insertTable, object: nil)
} }
@objc private func smartEval() { @objc private func smartEval() {
viewport?.sendCommand(4) NotificationCenter.default.post(name: .smartEval, object: nil)
} }
@objc private func openNote() { @objc private func openNote() {
@ -408,7 +412,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}() }()
@objc private func formatDocument() { @objc private func formatDocument() {
viewport?.sendCommand(10) NotificationCenter.default.post(name: .formatDocument, object: nil)
} }
@objc private func openSettings() { @objc private func openSettings() {
@ -417,24 +421,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@objc private func settingsDidChange() { @objc private func settingsDidChange() {
window.backgroundColor = Theme.current.base window.backgroundColor = Theme.current.base
syncThemeToViewport()
window.contentView?.needsDisplay = true window.contentView?.needsDisplay = true
} }
private func syncThemeToViewport() {
let mode = ConfigManager.shared.themeMode
let name: String
switch mode {
case "dark": name = "mocha"
case "light": name = "latte"
default:
let appearance = NSApp.effectiveAppearance
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
name = isDark ? "mocha" : "latte"
}
viewport?.setTheme(name)
}
@objc private func toggleBrowser() { @objc private func toggleBrowser() {
DocumentBrowserController.shared?.toggle() DocumentBrowserController.shared?.toggle()
} }

View File

@ -159,7 +159,7 @@ class IcedViewportView: NSView {
if cmd && !shift { if cmd && !shift {
switch chars { switch chars {
case "a", "b", "c", "e", "f", "g", "i", "v", "x", "z", "p", "t", case "a", "b", "c", "i", "v", "x", "z", "p", "t",
"=", "+", "-", "0": "=", "+", "-", "0":
keyDown(with: event) keyDown(with: event)
return true return true
@ -168,7 +168,7 @@ class IcedViewportView: NSView {
} }
if cmd && shift { if cmd && shift {
switch chars { switch chars {
case "g", "z": case "z":
keyDown(with: event) keyDown(with: event)
return true return true
default: break default: break
@ -214,16 +214,4 @@ class IcedViewportView: NSView {
viewport_free_string(cstr) viewport_free_string(cstr)
return result return result
} }
func sendCommand(_ command: UInt32) {
guard let h = viewportHandle else { return }
viewport_send_command(h, command)
}
func setTheme(_ name: String) {
guard let h = viewportHandle else { return }
name.withCString { cstr in
viewport_set_theme(h, cstr)
}
}
} }

View File

@ -53,8 +53,4 @@ char *viewport_get_text(struct ViewportHandle *handle);
void viewport_free_string(char *s); void viewport_free_string(char *s);
void viewport_set_theme(struct ViewportHandle *_handle, const char *name);
void viewport_send_command(struct ViewportHandle *handle, uint32_t command);
#endif /* ACORD_VIEWPORT_H */ #endif /* ACORD_VIEWPORT_H */

View File

@ -1,476 +0,0 @@
use std::sync::atomic::{AtomicU64, Ordering};
use iced_widget::text_editor;
static NEXT_BLOCK_ID: AtomicU64 = AtomicU64::new(1);
pub fn next_id() -> u64 {
NEXT_BLOCK_ID.fetch_add(1, Ordering::Relaxed)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockKind {
Text,
Table,
HorizontalRule,
Heading,
Tree,
EvalResult,
}
pub struct Block {
pub id: u64,
pub kind: BlockKind,
pub content: text_editor::Content<iced_wgpu::Renderer>,
pub start_line: usize,
pub line_count: usize,
pub heading_level: u8,
pub heading_text: String,
pub table_rows: Vec<Vec<String>>,
pub tree_data: Option<serde_json::Value>,
pub eval_text: String,
}
impl Block {
pub fn text(text: &str, start_line: usize) -> Self {
let lc = text.lines().count().max(1);
Self {
id: next_id(),
kind: BlockKind::Text,
content: text_editor::Content::with_text(text),
start_line,
line_count: lc,
heading_level: 0,
heading_text: String::new(),
table_rows: Vec::new(),
tree_data: None,
eval_text: String::new(),
}
}
pub fn heading(level: u8, text: String, line: usize) -> Self {
Self {
id: next_id(),
kind: BlockKind::Heading,
content: text_editor::Content::with_text(""),
start_line: line,
line_count: 1,
heading_level: level,
heading_text: text,
table_rows: Vec::new(),
tree_data: None,
eval_text: String::new(),
}
}
pub fn hr(line: usize) -> Self {
Self {
id: next_id(),
kind: BlockKind::HorizontalRule,
content: text_editor::Content::with_text(""),
start_line: line,
line_count: 1,
heading_level: 0,
heading_text: String::new(),
table_rows: Vec::new(),
tree_data: None,
eval_text: String::new(),
}
}
pub fn table(rows: Vec<Vec<String>>, start_line: usize, line_count: usize) -> Self {
Self {
id: next_id(),
kind: BlockKind::Table,
content: text_editor::Content::with_text(""),
start_line,
line_count,
heading_level: 0,
heading_text: String::new(),
table_rows: rows,
tree_data: None,
eval_text: String::new(),
}
}
pub fn eval_result(text: String, line: usize) -> Self {
Self {
id: next_id(),
kind: BlockKind::EvalResult,
content: text_editor::Content::with_text(""),
start_line: line,
line_count: 1,
heading_level: 0,
heading_text: String::new(),
table_rows: Vec::new(),
tree_data: None,
eval_text: text,
}
}
pub fn tree(data: serde_json::Value, line: usize) -> Self {
Self {
id: next_id(),
kind: BlockKind::Tree,
content: text_editor::Content::with_text(""),
start_line: line,
line_count: 1,
heading_level: 0,
heading_text: String::new(),
table_rows: Vec::new(),
tree_data: Some(data),
eval_text: String::new(),
}
}
}
fn is_hr_line(line: &str) -> bool {
let trimmed = line.trim();
trimmed.len() >= 3 && trimmed.chars().all(|c| c == '-')
}
fn heading_prefix(line: &str) -> Option<(u8, &str)> {
let trimmed = line.trim_start();
let bytes = trimmed.as_bytes();
if bytes.is_empty() || bytes[0] != b'#' {
return None;
}
let mut level = 0u8;
while (level as usize) < bytes.len() && bytes[level as usize] == b'#' {
level += 1;
}
if level > 3 || (level as usize) >= bytes.len() || bytes[level as usize] != b' ' {
return None;
}
Some((level, &trimmed[level as usize + 1..]))
}
fn is_table_start(lines: &[&str], idx: usize) -> bool {
if idx + 1 >= lines.len() {
return false;
}
let line = lines[idx].trim();
let next = lines[idx + 1].trim();
if !line.starts_with('|') || !next.starts_with('|') {
return false;
}
let inner = next.strip_prefix('|').unwrap_or(next);
let inner = inner.strip_suffix('|').unwrap_or(inner);
inner.split('|').all(|seg| {
let s = seg.trim();
!s.is_empty() && s.chars().all(|c| c == '-' || c == ':')
})
}
fn consume_table(lines: &[&str], start: usize) -> (Vec<Vec<String>>, usize) {
let parse_row = |line: &str| -> Vec<String> {
let trimmed = line.trim();
let inner = trimmed.strip_prefix('|').unwrap_or(trimmed);
let inner = inner.strip_suffix('|').unwrap_or(inner);
inner.split('|').map(|c| c.trim().to_string()).collect()
};
let mut rows = vec![parse_row(lines[start])];
let mut end = start + 2; // skip header + separator
while end < lines.len() {
let trimmed = lines[end].trim();
if trimmed.is_empty() || !trimmed.contains('|') {
break;
}
rows.push(parse_row(lines[end]));
end += 1;
}
(rows, end)
}
/// Classify each line into a block kind, returning (kind, line_index) boundaries.
struct BlockSpan {
kind: BlockKind,
start: usize,
end: usize, // exclusive
heading_level: u8,
heading_text: String,
table_rows: Vec<Vec<String>>,
}
fn classify_spans(lines: &[&str]) -> Vec<BlockSpan> {
let mut spans = Vec::new();
let mut i = 0;
let mut text_start: Option<usize> = None;
let flush_text = |text_start: &mut Option<usize>, i: usize, spans: &mut Vec<BlockSpan>| {
if let Some(s) = text_start.take() {
if s < i {
spans.push(BlockSpan {
kind: BlockKind::Text,
start: s,
end: i,
heading_level: 0,
heading_text: String::new(),
table_rows: Vec::new(),
});
}
}
};
while i < lines.len() {
if is_hr_line(lines[i]) {
flush_text(&mut text_start, i, &mut spans);
spans.push(BlockSpan {
kind: BlockKind::HorizontalRule,
start: i,
end: i + 1,
heading_level: 0,
heading_text: String::new(),
table_rows: Vec::new(),
});
i += 1;
} else if let Some((level, text)) = heading_prefix(lines[i]) {
flush_text(&mut text_start, i, &mut spans);
spans.push(BlockSpan {
kind: BlockKind::Heading,
start: i,
end: i + 1,
heading_level: level,
heading_text: text.to_string(),
table_rows: Vec::new(),
});
i += 1;
} else if is_table_start(lines, i) {
flush_text(&mut text_start, i, &mut spans);
let (rows, end) = consume_table(lines, i);
spans.push(BlockSpan {
kind: BlockKind::Table,
start: i,
end,
heading_level: 0,
heading_text: String::new(),
table_rows: rows,
});
i = end;
} else {
if text_start.is_none() {
text_start = Some(i);
}
i += 1;
}
}
flush_text(&mut text_start, lines.len(), &mut spans);
if spans.is_empty() {
spans.push(BlockSpan {
kind: BlockKind::Text,
start: 0,
end: 0,
heading_level: 0,
heading_text: String::new(),
table_rows: Vec::new(),
});
}
spans
}
/// Parse document text into blocks, detecting HR, headings, and tables.
pub fn parse_blocks(text: &str) -> Vec<Block> {
if text.is_empty() {
return vec![Block::text("", 0)];
}
let lines: Vec<&str> = text.lines().collect();
let spans = classify_spans(&lines);
let mut blocks = Vec::with_capacity(spans.len());
for span in &spans {
match span.kind {
BlockKind::Text => {
let block_text = lines[span.start..span.end].join("\n");
blocks.push(Block::text(&block_text, span.start));
}
BlockKind::HorizontalRule => {
blocks.push(Block::hr(span.start));
}
BlockKind::Heading => {
blocks.push(Block::heading(span.heading_level, span.heading_text.clone(), span.start));
}
BlockKind::Table => {
blocks.push(Block::table(span.table_rows.clone(), span.start, span.end - span.start));
}
_ => {}
}
}
if blocks.is_empty() {
blocks.push(Block::text("", 0));
}
blocks
}
/// Incremental reparse: compare old block kinds to new, reuse unchanged blocks.
/// Returns the new block list, preserving IDs and content of blocks that didn't change type.
pub fn reparse_incremental(old_blocks: &mut Vec<Block>, text: &str) {
let lines: Vec<&str> = if text.is_empty() {
Vec::new()
} else {
text.lines().collect()
};
let spans = classify_spans(&lines);
let old_kinds: Vec<(BlockKind, usize, usize)> = old_blocks
.iter()
.map(|b| (b.kind, b.start_line, b.line_count))
.collect();
let new_kinds: Vec<(BlockKind, usize, usize)> = spans
.iter()
.map(|s| (s.kind, s.start, s.end - s.start))
.collect();
if old_kinds == new_kinds {
// Same structure: update text content in-place for text blocks
for (block, span) in old_blocks.iter_mut().zip(spans.iter()) {
block.start_line = span.start;
block.line_count = span.end - span.start;
if block.kind == BlockKind::Text {
let block_text = lines[span.start..span.end].join("\n");
let current = block.content.text();
if current != block_text {
block.content = text_editor::Content::with_text(&block_text);
}
}
}
return;
}
// Structure changed: rebuild, but try to reuse blocks at matching positions
let mut new_blocks = Vec::with_capacity(spans.len());
for (i, span) in spans.iter().enumerate() {
let reuse = if i < old_blocks.len()
&& old_blocks[i].kind == span.kind
&& old_blocks[i].start_line == span.start
{
true
} else {
false
};
if reuse {
let mut b = std::mem::replace(&mut old_blocks[i], Block::text("", 0));
b.start_line = span.start;
b.line_count = span.end - span.start;
if b.kind == BlockKind::Text {
let block_text = lines[span.start..span.end].join("\n");
let current = b.content.text();
if current != block_text {
b.content = text_editor::Content::with_text(&block_text);
}
}
new_blocks.push(b);
} else {
match span.kind {
BlockKind::Text => {
let block_text = lines[span.start..span.end].join("\n");
new_blocks.push(Block::text(&block_text, span.start));
}
BlockKind::HorizontalRule => {
new_blocks.push(Block::hr(span.start));
}
BlockKind::Heading => {
new_blocks.push(Block::heading(span.heading_level, span.heading_text.clone(), span.start));
}
BlockKind::Table => {
new_blocks.push(Block::table(span.table_rows.clone(), span.start, span.end - span.start));
}
_ => {}
}
}
}
if new_blocks.is_empty() {
new_blocks.push(Block::text("", 0));
}
*old_blocks = new_blocks;
}
/// Serialize blocks back to document text.
pub fn serialize_blocks(blocks: &[Block]) -> String {
let mut parts = Vec::new();
for block in blocks {
match block.kind {
BlockKind::Text => {
parts.push(block.content.text());
}
BlockKind::Table => {
if let Some(header) = block.table_rows.first() {
let cells: Vec<&str> = header.iter().map(|s| s.as_str()).collect();
parts.push(format!("| {} |", cells.join(" | ")));
let sep = header.iter()
.map(|c| "-".repeat(c.len().max(3)))
.collect::<Vec<_>>()
.join(" | ");
parts.push(format!("| {} |", sep));
for row in block.table_rows.iter().skip(1) {
let cells: Vec<&str> = row.iter().map(|s| s.as_str()).collect();
parts.push(format!("| {} |", cells.join(" | ")));
}
}
}
BlockKind::HorizontalRule => {
parts.push("---".to_string());
}
BlockKind::Heading => {
let prefix = "#".repeat(block.heading_level as usize);
parts.push(format!("{prefix} {}", block.heading_text));
}
BlockKind::Tree => {}
BlockKind::EvalResult => {
parts.push(block.eval_text.clone());
}
}
}
parts.join("\n")
}
/// Extract plain text from all TextBlocks.
pub fn collect_text(blocks: &[Block]) -> String {
let mut parts = Vec::new();
for block in blocks {
if block.kind == BlockKind::Text {
parts.push(block.content.text());
}
}
parts.join("\n")
}
/// Merge two adjacent text blocks. Returns the merged text.
pub fn merge_text_blocks(first: &Block, second: &Block) -> String {
let a = first.content.text();
let b = second.content.text();
if a.is_empty() {
b
} else if b.is_empty() {
a
} else {
format!("{}\n{}", a, b)
}
}
/// Update start_line counts after blocks change.
pub fn recount_lines(blocks: &mut [Block]) {
let mut line = 0;
for block in blocks.iter_mut() {
block.start_line = line;
line += block.line_count;
}
}
/// Find the block index containing a given global line number.
pub fn block_at_line(blocks: &[Block], global_line: usize) -> Option<usize> {
for (i, block) in blocks.iter().enumerate() {
if global_line >= block.start_line && global_line < block.start_line + block.line_count {
return Some(i);
}
}
None
}

File diff suppressed because it is too large Load Diff

View File

@ -1,70 +0,0 @@
use iced_wgpu::core::text::LineHeight;
use iced_wgpu::core::{
alignment, Element, Font, Length, Pixels, Point, Rectangle, Theme,
};
use iced_widget::canvas;
use crate::palette;
const FONT_SIZE: f32 = 13.0;
const LINE_HEIGHT: f32 = 20.0;
const LEFT_MARGIN: f32 = 8.0;
const PADDING: f32 = 4.0;
pub struct EvalResultProgram {
text: String,
}
impl EvalResultProgram {
pub fn new(result_text: &str) -> Self {
Self {
text: format!("\u{2192} {}", result_text),
}
}
pub fn height() -> f32 {
LINE_HEIGHT + PADDING * 2.0
}
}
impl<Message: Clone> canvas::Program<Message, Theme, iced_wgpu::Renderer> for EvalResultProgram {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced_wgpu::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: iced_wgpu::core::mouse::Cursor,
) -> Vec<canvas::Geometry<iced_wgpu::Renderer>> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let p = palette::current();
frame.fill_text(canvas::Text {
content: self.text.clone(),
position: Point::new(LEFT_MARGIN, PADDING),
max_width: bounds.width - LEFT_MARGIN * 2.0,
color: p.green,
size: Pixels(FONT_SIZE),
line_height: LineHeight::Relative(1.3),
font: Font::MONOSPACE,
align_x: alignment::Horizontal::Left.into(),
align_y: alignment::Vertical::Top,
shaping: iced_wgpu::core::text::Shaping::Basic,
});
vec![frame.into_geometry()]
}
}
pub fn view<'a, Message: Clone + 'a>(
result_text: &str,
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
let program = EvalResultProgram::new(result_text);
let h = EvalResultProgram::height();
canvas::Canvas::new(program)
.width(Length::Fill)
.height(Length::Fixed(h))
.into()
}

View File

@ -12,7 +12,6 @@ use raw_window_handle::{
}; };
use crate::editor::{EditorState, Message}; use crate::editor::{EditorState, Message};
use crate::palette;
use crate::ViewportHandle; use crate::ViewportHandle;
struct MacClipboard; struct MacClipboard;
@ -159,35 +158,22 @@ pub fn render(handle: &mut ViewportHandle) {
let mut messages: Vec<Message> = Vec::new(); let mut messages: Vec<Message> = Vec::new();
for event in &handle.events { for event in &handle.events {
match event { if let Event::Keyboard(keyboard::Event::KeyPressed {
Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Character(c),
key: keyboard::Key::Character(c), modifiers,
modifiers, ..
.. }) = event
}) if modifiers.logo() => { {
if modifiers.logo() {
match c.as_str() { match c.as_str() {
"p" => messages.push(Message::TogglePreview), "p" => messages.push(Message::TogglePreview),
"t" => messages.push(Message::InsertTable), "t" => messages.push(Message::InsertTable),
"b" => messages.push(Message::ToggleBold), "b" => messages.push(Message::ToggleBold),
"i" => messages.push(Message::ToggleItalic), "i" => messages.push(Message::ToggleItalic),
"e" => messages.push(Message::SmartEval), "e" => messages.push(Message::SmartEval),
"z" if modifiers.shift() => messages.push(Message::Redo),
"z" => messages.push(Message::Undo),
"f" => messages.push(Message::ToggleFind),
"g" if modifiers.shift() => messages.push(Message::FindPrev),
"g" => messages.push(Message::FindNext),
_ => {} _ => {}
} }
} }
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Escape),
..
}) => {
if handle.state.find.visible {
messages.push(Message::HideFind);
}
}
_ => {}
} }
} }
@ -206,9 +192,6 @@ pub fn render(handle: &mut ViewportHandle) {
handle.state.update(msg); handle.state.update(msg);
} }
handle.state.tick();
let pending_focus = handle.state.take_pending_focus();
let theme = Theme::Dark; let theme = Theme::Dark;
let style = Style { let style = Style {
text_color: Color::WHITE, text_color: Color::WHITE,
@ -221,18 +204,13 @@ pub fn render(handle: &mut ViewportHandle) {
&mut handle.renderer, &mut handle.renderer,
); );
if let Some(focus_id) = pending_focus {
use iced_wgpu::core::widget::operation::focusable;
let mut op = focusable::focus(focus_id);
ui.operate(&handle.renderer, &mut op);
}
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor); ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
handle.cache = ui.into_cache(); handle.cache = ui.into_cache();
let bg = Color::from_rgb(0.08, 0.08, 0.10);
handle handle
.renderer .renderer
.present(Some(palette::current().base), handle.format, &view, &handle.viewport); .present(Some(bg), handle.format, &view, &handle.viewport);
frame.present(); frame.present();
} }

View File

@ -1,116 +0,0 @@
use iced_wgpu::core::alignment;
use iced_wgpu::core::text::LineHeight;
use iced_wgpu::core::{
mouse, Element, Font, Length, Pixels, Point, Rectangle, Theme,
};
use iced_wgpu::core::font::Weight;
use iced_widget::canvas;
use crate::palette;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeadingLevel {
H1,
H2,
H3,
}
impl HeadingLevel {
pub fn scale(&self) -> f32 {
match self {
HeadingLevel::H1 => 2.0,
HeadingLevel::H2 => 1.5,
HeadingLevel::H3 => 1.17,
}
}
pub fn weight(&self) -> Weight {
match self {
HeadingLevel::H1 => Weight::Black,
HeadingLevel::H2 => Weight::Bold,
HeadingLevel::H3 => Weight::Semibold,
}
}
pub fn from_prefix(line: &str) -> Option<(HeadingLevel, String)> {
let trimmed = line.trim_start();
let bytes = trimmed.as_bytes();
if bytes.is_empty() || bytes[0] != b'#' {
return None;
}
let mut level = 0;
while level < bytes.len() && bytes[level] == b'#' {
level += 1;
}
if level > 3 || level >= bytes.len() || bytes[level] != b' ' {
return None;
}
let text = trimmed[level + 1..].to_string();
let lvl = match level {
1 => HeadingLevel::H1,
2 => HeadingLevel::H2,
_ => HeadingLevel::H3,
};
Some((lvl, text))
}
}
struct HeadingProgram {
level: HeadingLevel,
text: String,
font_size: f32,
}
impl<Message: Clone> canvas::Program<Message, Theme, iced_wgpu::Renderer> for HeadingProgram {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced_wgpu::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry<iced_wgpu::Renderer>> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let p = palette::current();
let color = match self.level {
HeadingLevel::H1 => p.rosewater,
HeadingLevel::H2 => p.peach,
HeadingLevel::H3 => p.yellow,
};
frame.fill_text(canvas::Text {
content: self.text.clone(),
position: Point::new(8.0, 4.0),
max_width: bounds.width - 16.0,
color,
size: Pixels(self.font_size),
line_height: LineHeight::Relative(1.4),
font: Font { weight: self.level.weight(), ..Font::DEFAULT },
align_x: iced_wgpu::core::text::Alignment::Left,
align_y: alignment::Vertical::Top,
shaping: iced_wgpu::core::text::Shaping::Basic,
});
vec![frame.into_geometry()]
}
}
pub fn view<'a, Message: Clone + 'a>(
level: HeadingLevel,
text: &str,
base_font_size: f32,
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
let font_size = base_font_size * level.scale();
let height = font_size * 1.4 + 8.0;
canvas::Canvas::new(HeadingProgram {
level,
text: text.to_string(),
font_size,
})
.width(Length::Fill)
.height(Length::Fixed(height))
.into()
}

View File

@ -1,42 +0,0 @@
use iced_wgpu::core::{mouse, Element, Length, Point, Rectangle, Theme};
use iced_widget::canvas;
use crate::palette;
struct HRProgram;
impl<Message: Clone> canvas::Program<Message, Theme, iced_wgpu::Renderer> for HRProgram {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced_wgpu::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry<iced_wgpu::Renderer>> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let p = palette::current();
let y = bounds.height / 2.0;
let margin = 8.0;
let path = canvas::Path::line(
Point::new(margin, y),
Point::new(bounds.width - margin, y),
);
frame.stroke(
&path,
canvas::Stroke::default()
.with_width(1.0)
.with_color(p.overlay0),
);
vec![frame.into_geometry()]
}
}
pub fn view<'a, Message: Clone + 'a>() -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
canvas::Canvas::new(HRProgram)
.width(Length::Fill)
.height(Length::Fixed(20.0))
.into()
}

View File

@ -1,16 +1,9 @@
use std::ffi::{c_char, c_void, CStr, CString}; use std::ffi::{c_char, c_void, CStr, CString};
pub mod blocks;
mod bridge; mod bridge;
mod editor; mod editor;
pub mod eval_block;
mod handle; mod handle;
pub mod heading_block;
pub mod hr_block;
pub mod palette;
mod syntax; mod syntax;
pub mod table_block;
pub mod tree_block;
pub use acord_core::*; pub use acord_core::*;
@ -179,34 +172,3 @@ pub extern "C" fn viewport_free_string(s: *mut c_char) {
if s.is_null() { return; } if s.is_null() { return; }
unsafe { drop(CString::from_raw(s)); } unsafe { drop(CString::from_raw(s)); }
} }
#[no_mangle]
pub extern "C" fn viewport_set_theme(_handle: *mut ViewportHandle, name: *const c_char) {
let s = if name.is_null() {
"mocha"
} else {
unsafe { CStr::from_ptr(name) }.to_str().unwrap_or("mocha")
};
palette::set_theme(s);
}
#[no_mangle]
pub extern "C" fn viewport_send_command(handle: *mut ViewportHandle, command: u32) {
let h = match unsafe { handle.as_mut() } {
Some(h) => h,
None => return,
};
let msg = match command {
1 => editor::Message::ToggleBold,
2 => editor::Message::ToggleItalic,
3 => editor::Message::InsertTable,
4 => editor::Message::SmartEval,
5 => editor::Message::Evaluate,
6 => editor::Message::TogglePreview,
7 => editor::Message::ZoomIn,
8 => editor::Message::ZoomOut,
9 => editor::Message::ZoomReset,
_ => return,
};
h.state.update(msg);
}

View File

@ -1,106 +0,0 @@
use iced_wgpu::core::Color;
use std::cell::RefCell;
#[derive(Clone, Copy)]
pub struct Palette {
pub rosewater: Color,
pub flamingo: Color,
pub pink: Color,
pub mauve: Color,
pub red: Color,
pub maroon: Color,
pub peach: Color,
pub yellow: Color,
pub green: Color,
pub teal: Color,
pub sky: Color,
pub sapphire: Color,
pub blue: Color,
pub lavender: Color,
pub text: Color,
pub subtext1: Color,
pub subtext0: Color,
pub overlay2: Color,
pub overlay1: Color,
pub overlay0: Color,
pub surface2: Color,
pub surface1: Color,
pub surface0: Color,
pub base: Color,
pub mantle: Color,
pub crust: Color,
}
pub static MOCHA: Palette = Palette {
rosewater: Color::from_rgb(0.961, 0.878, 0.863),
flamingo: Color::from_rgb(0.949, 0.804, 0.804),
pink: Color::from_rgb(0.961, 0.761, 0.906),
mauve: Color::from_rgb(0.796, 0.651, 0.969),
red: Color::from_rgb(0.953, 0.545, 0.659),
maroon: Color::from_rgb(0.922, 0.627, 0.675),
peach: Color::from_rgb(0.980, 0.702, 0.529),
yellow: Color::from_rgb(0.976, 0.886, 0.686),
green: Color::from_rgb(0.651, 0.890, 0.631),
teal: Color::from_rgb(0.580, 0.886, 0.835),
sky: Color::from_rgb(0.537, 0.863, 0.922),
sapphire: Color::from_rgb(0.455, 0.780, 0.925),
blue: Color::from_rgb(0.537, 0.706, 0.980),
lavender: Color::from_rgb(0.706, 0.745, 0.996),
text: Color::from_rgb(0.804, 0.839, 0.957),
subtext1: Color::from_rgb(0.729, 0.761, 0.871),
subtext0: Color::from_rgb(0.651, 0.678, 0.784),
overlay2: Color::from_rgb(0.576, 0.600, 0.698),
overlay1: Color::from_rgb(0.498, 0.518, 0.612),
overlay0: Color::from_rgb(0.424, 0.439, 0.525),
surface2: Color::from_rgb(0.345, 0.357, 0.439),
surface1: Color::from_rgb(0.271, 0.278, 0.353),
surface0: Color::from_rgb(0.192, 0.196, 0.267),
base: Color::from_rgb(0.118, 0.118, 0.180),
mantle: Color::from_rgb(0.094, 0.094, 0.145),
crust: Color::from_rgb(0.067, 0.067, 0.106),
};
pub static LATTE: Palette = Palette {
rosewater: Color::from_rgb(0.863, 0.541, 0.471),
flamingo: Color::from_rgb(0.867, 0.471, 0.471),
pink: Color::from_rgb(0.918, 0.463, 0.796),
mauve: Color::from_rgb(0.533, 0.224, 0.937),
red: Color::from_rgb(0.824, 0.059, 0.224),
maroon: Color::from_rgb(0.902, 0.271, 0.325),
peach: Color::from_rgb(0.996, 0.392, 0.043),
yellow: Color::from_rgb(0.875, 0.557, 0.114),
green: Color::from_rgb(0.251, 0.627, 0.169),
teal: Color::from_rgb(0.090, 0.573, 0.600),
sky: Color::from_rgb(0.016, 0.647, 0.898),
sapphire: Color::from_rgb(0.125, 0.624, 0.710),
blue: Color::from_rgb(0.118, 0.400, 0.961),
lavender: Color::from_rgb(0.447, 0.529, 0.992),
text: Color::from_rgb(0.298, 0.310, 0.412),
subtext1: Color::from_rgb(0.361, 0.373, 0.467),
subtext0: Color::from_rgb(0.424, 0.435, 0.522),
overlay2: Color::from_rgb(0.486, 0.498, 0.576),
overlay1: Color::from_rgb(0.549, 0.561, 0.631),
overlay0: Color::from_rgb(0.612, 0.627, 0.690),
surface2: Color::from_rgb(0.675, 0.690, 0.745),
surface1: Color::from_rgb(0.737, 0.753, 0.800),
surface0: Color::from_rgb(0.800, 0.816, 0.855),
base: Color::from_rgb(0.937, 0.945, 0.961),
mantle: Color::from_rgb(0.902, 0.914, 0.937),
crust: Color::from_rgb(0.863, 0.878, 0.910),
};
thread_local! {
static CURRENT: RefCell<&'static Palette> = const { RefCell::new(&MOCHA) };
}
pub fn current() -> &'static Palette {
CURRENT.with(|c| *c.borrow())
}
pub fn set_theme(name: &str) {
let pal = match name {
"latte" | "light" => &LATTE,
_ => &MOCHA,
};
CURRENT.with(|c| *c.borrow_mut() = pal);
}

View File

@ -1,36 +1,13 @@
use std::ops::Range; use std::ops::Range;
use iced_wgpu::core::text::highlighter; use iced_wgpu::core::text::highlighter;
use iced_wgpu::core::{Color, Font}; use iced_wgpu::core::Color;
use iced_wgpu::core::font::{Weight, Style as FontStyle};
use acord_core::highlight::{highlight_source, HighlightSpan}; use acord_core::highlight::{highlight_source, HighlightSpan};
use acord_core::doc::{classify_document, LineKind};
use crate::editor::{RESULT_PREFIX, ERROR_PREFIX}; use crate::editor::{RESULT_PREFIX, ERROR_PREFIX};
use crate::palette;
pub const EVAL_RESULT_KIND: u8 = 24; pub const EVAL_RESULT_KIND: u8 = 24;
pub const EVAL_ERROR_KIND: u8 = 25; pub const EVAL_ERROR_KIND: u8 = 25;
const MD_HEADING_MARKER: u8 = 26;
const MD_H1: u8 = 27;
const MD_H2: u8 = 28;
const MD_H3: u8 = 29;
const MD_BOLD: u8 = 30;
const MD_ITALIC: u8 = 31;
const MD_INLINE_CODE: u8 = 32;
const MD_FORMAT_MARKER: u8 = 33;
const MD_LINK_TEXT: u8 = 34;
const MD_LINK_URL: u8 = 35;
const MD_BLOCKQUOTE_MARKER: u8 = 36;
const MD_BLOCKQUOTE: u8 = 37;
const MD_LIST_MARKER: u8 = 38;
const MD_FENCE_MARKER: u8 = 39;
const MD_CODE_BLOCK: u8 = 40;
const MD_HR: u8 = 41;
const MD_TASK_OPEN: u8 = 42;
const MD_TASK_DONE: u8 = 43;
const MD_BOLD_ITALIC: u8 = 44;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct SyntaxSettings { pub struct SyntaxSettings {
pub lang: String, pub lang: String,
@ -42,23 +19,11 @@ pub struct SyntaxHighlight {
pub kind: u8, pub kind: u8,
} }
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LineDecor {
None,
CodeBlock,
Blockquote,
HorizontalRule,
FenceMarker,
}
pub struct SyntaxHighlighter { pub struct SyntaxHighlighter {
lang: String, lang: String,
spans: Vec<HighlightSpan>, spans: Vec<HighlightSpan>,
line_offsets: Vec<usize>, line_offsets: Vec<usize>,
line_kinds: Vec<LineKind>,
in_fenced_code: bool,
current_line: usize, current_line: usize,
line_decors: Vec<LineDecor>,
} }
impl SyntaxHighlighter { impl SyntaxHighlighter {
@ -70,323 +35,8 @@ impl SyntaxHighlighter {
self.line_offsets.push(offset); self.line_offsets.push(offset);
offset += line.len() + 1; offset += line.len() + 1;
} }
let classified = classify_document(source);
self.line_kinds = classified.into_iter().map(|cl| cl.kind).collect();
self.line_decors.clear();
let mut in_fence = false;
for (i, raw_line) in source.split('\n').enumerate() {
let is_md = i < self.line_kinds.len() && self.line_kinds[i] == LineKind::Markdown;
if is_md {
let trimmed = raw_line.trim_start();
if trimmed.starts_with("```") {
in_fence = !in_fence;
self.line_decors.push(LineDecor::FenceMarker);
} else if in_fence {
self.line_decors.push(LineDecor::CodeBlock);
} else if is_horizontal_rule(trimmed) {
self.line_decors.push(LineDecor::HorizontalRule);
} else if trimmed.starts_with("> ") || trimmed == ">" {
self.line_decors.push(LineDecor::Blockquote);
} else {
self.line_decors.push(LineDecor::None);
}
} else {
if in_fence { in_fence = false; }
self.line_decors.push(LineDecor::None);
}
}
self.in_fenced_code = false;
self.current_line = 0; self.current_line = 0;
} }
fn highlight_markdown(&self, line: &str) -> Vec<(Range<usize>, SyntaxHighlight)> {
let trimmed = line.trim_start();
let leading = line.len() - trimmed.len();
if is_horizontal_rule(trimmed) {
return vec![(0..line.len(), SyntaxHighlight { kind: MD_HR })];
}
if let Some(level) = heading_level(trimmed) {
let marker_end = leading + level + 1;
let kind = match level {
1 => MD_H1,
2 => MD_H2,
_ => MD_H3,
};
let mut spans = vec![
(0..marker_end, SyntaxHighlight { kind: MD_HEADING_MARKER }),
];
if marker_end < line.len() {
spans.push((marker_end..line.len(), SyntaxHighlight { kind }));
}
return spans;
}
if trimmed.starts_with("> ") || trimmed == ">" {
let marker_end = leading + if trimmed.len() > 1 { 2 } else { 1 };
let mut spans = vec![
(0..marker_end, SyntaxHighlight { kind: MD_BLOCKQUOTE_MARKER }),
];
if marker_end < line.len() {
let content = &line[marker_end..];
let inner = parse_inline(content, marker_end);
if inner.is_empty() {
spans.push((marker_end..line.len(), SyntaxHighlight { kind: MD_BLOCKQUOTE }));
} else {
spans.extend(inner);
}
}
return spans;
}
if let Some(list_info) = list_marker_info(trimmed) {
let (marker_len, marker_kind) = match list_info {
ListKind::TaskOpen(n) => (n, MD_TASK_OPEN),
ListKind::TaskDone(n) => (n, MD_TASK_DONE),
ListKind::Plain(n) => (n, MD_LIST_MARKER),
};
let marker_end = leading + marker_len;
let mut spans = vec![
(0..marker_end, SyntaxHighlight { kind: marker_kind }),
];
if marker_end < line.len() {
let content = &line[marker_end..];
let inner = parse_inline(content, marker_end);
if inner.is_empty() {
return spans;
}
spans.extend(inner);
}
return spans;
}
parse_inline(line, 0)
}
}
fn heading_level(trimmed: &str) -> Option<usize> {
let bytes = trimmed.as_bytes();
if bytes.is_empty() || bytes[0] != b'#' { return None; }
let mut level = 0;
while level < bytes.len() && bytes[level] == b'#' { level += 1; }
if level > 3 { return None; }
if level < bytes.len() && bytes[level] == b' ' {
Some(level)
} else {
None
}
}
fn is_horizontal_rule(trimmed: &str) -> bool {
if trimmed.len() < 3 { return false; }
let first = trimmed.as_bytes()[0];
if !matches!(first, b'-' | b'*' | b'_') { return false; }
trimmed.bytes().all(|b| b == first || b == b' ')
}
#[derive(Clone, Copy, PartialEq)]
enum ListKind {
Plain(usize),
TaskOpen(usize),
TaskDone(usize),
}
fn list_marker_info(trimmed: &str) -> Option<ListKind> {
let bytes = trimmed.as_bytes();
if bytes.is_empty() { return None; }
if matches!(bytes[0], b'-' | b'*' | b'+') && bytes.get(1) == Some(&b' ') {
if trimmed.starts_with("- [ ] ") {
return Some(ListKind::TaskOpen(6));
}
if trimmed.starts_with("- [x] ") || trimmed.starts_with("- [X] ") {
return Some(ListKind::TaskDone(6));
}
return Some(ListKind::Plain(2));
}
let mut i = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
if i > 0 && i < bytes.len() && matches!(bytes[i], b'.' | b')') {
if bytes.get(i + 1) == Some(&b' ') {
return Some(ListKind::Plain(i + 2));
}
}
None
}
fn parse_inline(text: &str, base: usize) -> Vec<(Range<usize>, SyntaxHighlight)> {
let bytes = text.as_bytes();
let len = bytes.len();
let mut spans = Vec::new();
let mut i = 0;
while i < len {
if bytes[i] == b'\\' && i + 1 < len && is_md_punctuation(bytes[i + 1]) {
i += 2;
continue;
}
if i + 2 < len && bytes[i] == b'*' && bytes[i + 1] == b'*' && bytes[i + 2] == b'*' {
if let Some(end) = find_triple_star(bytes, i + 3) {
spans.push((base + i..base + i + 3, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
if i + 3 < end {
spans.push((base + i + 3..base + end, SyntaxHighlight { kind: MD_BOLD_ITALIC }));
}
spans.push((base + end..base + end + 3, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
i = end + 3;
continue;
}
}
if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'*' {
if let Some(end) = find_closing(bytes, i + 2, b'*', b'*') {
spans.push((base + i..base + i + 2, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
if i + 2 < end {
let inner = parse_inline(&text[i + 2..end], base + i + 2);
if inner.is_empty() {
spans.push((base + i + 2..base + end, SyntaxHighlight { kind: MD_BOLD }));
} else {
for (r, h) in inner {
let kind = if h.kind == MD_ITALIC { MD_BOLD_ITALIC } else { h.kind };
spans.push((r, SyntaxHighlight { kind }));
}
}
}
spans.push((base + end..base + end + 2, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
i = end + 2;
continue;
}
}
if bytes[i] == b'*' && (i + 1 >= len || bytes[i + 1] != b'*') {
if let Some(end) = find_single_closing(bytes, i + 1, b'*') {
if end > i + 1 && bytes[end - 1] != b'*' {
spans.push((base + i..base + i + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
if i + 1 < end {
spans.push((base + i + 1..base + end, SyntaxHighlight { kind: MD_ITALIC }));
}
spans.push((base + end..base + end + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
i = end + 1;
continue;
}
}
}
if bytes[i] == b'`' {
let tick_count = count_backticks(bytes, i);
if let Some(end) = find_backtick_close(bytes, i + tick_count, tick_count) {
spans.push((base + i..base + i + tick_count, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
if i + tick_count < end {
spans.push((base + i + tick_count..base + end, SyntaxHighlight { kind: MD_INLINE_CODE }));
}
spans.push((base + end..base + end + tick_count, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
i = end + tick_count;
continue;
}
}
if bytes[i] == b'[' {
if let Some((text_end, url_end)) = find_link(bytes, i) {
spans.push((base + i..base + i + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
if i + 1 < text_end {
spans.push((base + i + 1..base + text_end, SyntaxHighlight { kind: MD_LINK_TEXT }));
}
spans.push((base + text_end..base + text_end + 2, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
if text_end + 2 < url_end {
spans.push((base + text_end + 2..base + url_end, SyntaxHighlight { kind: MD_LINK_URL }));
}
spans.push((base + url_end..base + url_end + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
i = url_end + 1;
continue;
}
}
i += 1;
}
spans
}
fn is_md_punctuation(b: u8) -> bool {
matches!(b, b'\\' | b'`' | b'*' | b'_' | b'{' | b'}' | b'[' | b']'
| b'(' | b')' | b'#' | b'+' | b'-' | b'.' | b'!' | b'|')
}
fn find_triple_star(bytes: &[u8], start: usize) -> Option<usize> {
let mut i = start;
while i + 2 < bytes.len() {
if bytes[i] == b'*' && bytes[i + 1] == b'*' && bytes[i + 2] == b'*' {
return Some(i);
}
i += 1;
}
None
}
fn count_backticks(bytes: &[u8], start: usize) -> usize {
let mut n = 0;
while start + n < bytes.len() && bytes[start + n] == b'`' { n += 1; }
n
}
fn find_backtick_close(bytes: &[u8], start: usize, count: usize) -> Option<usize> {
if count == 0 { return None; }
let mut i = start;
while i + count <= bytes.len() {
if count_backticks(bytes, i) == count {
return Some(i);
}
i += 1;
}
None
}
fn find_closing(bytes: &[u8], start: usize, c1: u8, c2: u8) -> Option<usize> {
let mut i = start;
while i + 1 < bytes.len() {
if bytes[i] == c1 && bytes[i + 1] == c2 {
return Some(i);
}
i += 1;
}
None
}
fn find_single_closing(bytes: &[u8], start: usize, ch: u8) -> Option<usize> {
let mut i = start;
while i < bytes.len() {
if bytes[i] == ch {
return Some(i);
}
i += 1;
}
None
}
fn find_link(bytes: &[u8], open: usize) -> Option<(usize, usize)> {
let mut i = open + 1;
while i < bytes.len() {
if bytes[i] == b']' {
if i + 1 < bytes.len() && bytes[i + 1] == b'(' {
let text_end = i;
let mut j = i + 2;
while j < bytes.len() {
if bytes[j] == b')' {
return Some((text_end, j));
}
j += 1;
}
}
return None;
}
if bytes[i] == b'\n' { return None; }
i += 1;
}
None
} }
impl highlighter::Highlighter for SyntaxHighlighter { impl highlighter::Highlighter for SyntaxHighlighter {
@ -399,10 +49,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
lang: settings.lang.clone(), lang: settings.lang.clone(),
spans: Vec::new(), spans: Vec::new(),
line_offsets: Vec::new(), line_offsets: Vec::new(),
line_kinds: Vec::new(),
in_fenced_code: false,
current_line: 0, current_line: 0,
line_decors: Vec::new(),
}; };
h.rebuild(&settings.source); h.rebuild(&settings.source);
h h
@ -415,42 +62,18 @@ impl highlighter::Highlighter for SyntaxHighlighter {
fn change_line(&mut self, line: usize) { fn change_line(&mut self, line: usize) {
self.current_line = self.current_line.min(line); self.current_line = self.current_line.min(line);
if line == 0 {
self.in_fenced_code = false;
}
} }
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> {
let ln = self.current_line; let ln = self.current_line;
self.current_line += 1; self.current_line += 1;
let trimmed = line.trim_start(); let trimmed = _line.trim_start();
if trimmed.starts_with(RESULT_PREFIX) { if trimmed.starts_with(RESULT_PREFIX) {
return vec![(0..line.len(), SyntaxHighlight { kind: EVAL_RESULT_KIND })].into_iter(); return vec![(0.._line.len(), SyntaxHighlight { kind: EVAL_RESULT_KIND })].into_iter();
} }
if trimmed.starts_with(ERROR_PREFIX) { if trimmed.starts_with(ERROR_PREFIX) {
return vec![(0..line.len(), SyntaxHighlight { kind: EVAL_ERROR_KIND })].into_iter(); return vec![(0.._line.len(), SyntaxHighlight { kind: EVAL_ERROR_KIND })].into_iter();
}
let is_markdown = ln < self.line_kinds.len()
&& self.line_kinds[ln] == LineKind::Markdown;
if is_markdown {
if trimmed.starts_with("```") {
self.in_fenced_code = !self.in_fenced_code;
return vec![(0..line.len(), SyntaxHighlight { kind: MD_FENCE_MARKER })].into_iter();
}
if self.in_fenced_code {
return vec![(0..line.len(), SyntaxHighlight { kind: MD_CODE_BLOCK })].into_iter();
}
let md_spans = self.highlight_markdown(line);
if !md_spans.is_empty() {
return md_spans.into_iter();
}
} else if self.in_fenced_code {
self.in_fenced_code = false;
} }
if ln >= self.line_offsets.len() { if ln >= self.line_offsets.len() {
@ -461,7 +84,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
let line_end = if ln + 1 < self.line_offsets.len() { let line_end = if ln + 1 < self.line_offsets.len() {
self.line_offsets[ln + 1] - 1 self.line_offsets[ln + 1] - 1
} else { } else {
line_start + line.len() line_start + _line.len()
}; };
let mut result = Vec::new(); let mut result = Vec::new();
@ -484,101 +107,33 @@ impl highlighter::Highlighter for SyntaxHighlighter {
} }
pub fn highlight_color(kind: u8) -> Color { pub fn highlight_color(kind: u8) -> Color {
let p = palette::current();
match kind { match kind {
0 => p.mauve, 0 => Color::from_rgb(0.804, 0.569, 0.945), // keyword - mauve
1 => p.blue, 1 => Color::from_rgb(0.537, 0.706, 0.980), // function - blue
2 => p.teal, 2 => Color::from_rgb(0.604, 0.831, 0.898), // function.builtin - teal
3 => p.yellow, 3 => Color::from_rgb(0.976, 0.827, 0.522), // type - yellow
4 => p.yellow, 4 => Color::from_rgb(0.976, 0.827, 0.522), // type.builtin - yellow
5 => p.teal, 5 => Color::from_rgb(0.569, 0.878, 0.800), // constructor - teal
6 => p.peach, 6 => Color::from_rgb(0.988, 0.702, 0.529), // constant - peach
7 => p.peach, 7 => Color::from_rgb(0.988, 0.702, 0.529), // constant.builtin - peach
8 => p.green, 8 => Color::from_rgb(0.651, 0.890, 0.631), // string - green
9 => p.peach, 9 => Color::from_rgb(0.988, 0.702, 0.529), // number - peach
10 => p.overlay0, 10 => Color::from_rgb(0.424, 0.443, 0.537), // comment - overlay0
11 => p.text, 11 => Color::from_rgb(0.804, 0.839, 0.957), // variable - text
12 => p.red, 12 => Color::from_rgb(0.949, 0.604, 0.584), // variable.builtin - red
13 => p.flamingo, 13 => Color::from_rgb(0.949, 0.773, 0.584), // variable.parameter - flamingo
14 => p.sky, 14 => Color::from_rgb(0.604, 0.831, 0.898), // operator - sky
15 => p.overlay2, 15 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation - overlay2
16 => p.overlay2, 16 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation.bracket - overlay2
17 => p.overlay2, 17 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation.delimiter - overlay2
18 => p.blue, 18 => Color::from_rgb(0.537, 0.706, 0.980), // property - blue
19 => p.mauve, 19 => Color::from_rgb(0.804, 0.569, 0.945), // tag - mauve
20 => p.yellow, 20 => Color::from_rgb(0.976, 0.827, 0.522), // attribute - yellow
21 => p.teal, 21 => Color::from_rgb(0.569, 0.878, 0.800), // label - teal
22 => p.red, 22 => Color::from_rgb(0.949, 0.604, 0.584), // escape - red
23 => p.text, 23 => Color::from_rgb(0.804, 0.839, 0.957), // embedded - text
24 => p.green, 24 => Color::from_rgb(0.651, 0.890, 0.631), // eval result - green
25 => p.maroon, 25 => Color::from_rgb(0.890, 0.400, 0.400), // eval error - muted red
MD_HEADING_MARKER => p.overlay0, _ => Color::from_rgb(0.804, 0.839, 0.957), // default text
MD_H1 => p.rosewater,
MD_H2 => p.peach,
MD_H3 => p.yellow,
MD_BOLD => p.peach,
MD_ITALIC => p.mauve,
MD_INLINE_CODE => p.green,
MD_FORMAT_MARKER => p.overlay0,
MD_LINK_TEXT => p.blue,
MD_LINK_URL => p.overlay1,
MD_BLOCKQUOTE_MARKER => p.overlay0,
MD_BLOCKQUOTE => p.sky,
MD_LIST_MARKER => p.sky,
MD_FENCE_MARKER => p.overlay0,
MD_CODE_BLOCK => p.text,
MD_HR => p.overlay1,
MD_TASK_OPEN => p.overlay2,
MD_TASK_DONE => p.green,
MD_BOLD_ITALIC => p.peach,
_ => p.text,
} }
} }
pub fn highlight_font(kind: u8) -> Option<Font> {
match kind {
MD_HEADING_MARKER => Some(Font { weight: Weight::Bold, ..Font::MONOSPACE }),
MD_H1 => Some(Font { weight: Weight::Black, ..Font::DEFAULT }),
MD_H2 => Some(Font { weight: Weight::Bold, ..Font::DEFAULT }),
MD_H3 => Some(Font { weight: Weight::Semibold, ..Font::DEFAULT }),
MD_BOLD => Some(Font { weight: Weight::Bold, ..Font::DEFAULT }),
MD_ITALIC => Some(Font { style: FontStyle::Italic, ..Font::DEFAULT }),
MD_BOLD_ITALIC => Some(Font { weight: Weight::Bold, style: FontStyle::Italic, ..Font::DEFAULT }),
MD_INLINE_CODE => Some(Font::MONOSPACE),
MD_FORMAT_MARKER => Some(Font::MONOSPACE),
MD_BLOCKQUOTE => Some(Font { style: FontStyle::Italic, ..Font::DEFAULT }),
MD_FENCE_MARKER => Some(Font::MONOSPACE),
MD_CODE_BLOCK => Some(Font::MONOSPACE),
MD_TASK_DONE => Some(Font { weight: Weight::Bold, ..Font::MONOSPACE }),
_ => None,
}
}
pub fn compute_line_decors(source: &str) -> Vec<LineDecor> {
let classified = classify_document(source);
let line_kinds: Vec<LineKind> = classified.into_iter().map(|cl| cl.kind).collect();
let mut decors = Vec::new();
let mut in_fence = false;
for (i, raw_line) in source.split('\n').enumerate() {
let is_md = i < line_kinds.len() && line_kinds[i] == LineKind::Markdown;
if is_md {
let trimmed = raw_line.trim_start();
if trimmed.starts_with("```") {
in_fence = !in_fence;
decors.push(LineDecor::FenceMarker);
} else if in_fence {
decors.push(LineDecor::CodeBlock);
} else if is_horizontal_rule(trimmed) {
decors.push(LineDecor::HorizontalRule);
} else if trimmed.starts_with("> ") || trimmed == ">" {
decors.push(LineDecor::Blockquote);
} else {
decors.push(LineDecor::None);
}
} else {
if in_fence { in_fence = false; }
decors.push(LineDecor::None);
}
}
decors
}

View File

@ -1,329 +0,0 @@
use iced_wgpu::core::widget::Id as WidgetId;
use iced_wgpu::core::{
Background, Border, Color, Element, Font, Length,
Padding, Shadow, Theme,
};
use iced_widget::container;
use iced_widget::text_input;
use crate::palette;
const MIN_COL_WIDTH: f32 = 60.0;
const DEFAULT_COL_WIDTH: f32 = 120.0;
const CELL_PADDING: Padding = Padding {
top: 3.0,
right: 6.0,
bottom: 3.0,
left: 6.0,
};
#[derive(Debug, Clone)]
pub enum TableMessage {
CellChanged(usize, usize, String),
FocusCell(usize, usize),
AddRow,
AddColumn,
}
pub struct TableState {
pub rows: Vec<Vec<String>>,
pub col_widths: Vec<f32>,
pub focused_cell: Option<(usize, usize)>,
}
impl TableState {
pub fn new(rows: Vec<Vec<String>>, col_widths: Option<Vec<f32>>) -> Self {
let col_count = rows.iter().map(|r| r.len()).max().unwrap_or(0);
let widths = col_widths.unwrap_or_else(|| vec![DEFAULT_COL_WIDTH; col_count]);
Self {
rows,
col_widths: widths,
focused_cell: None,
}
}
pub fn from_markdown(text: &str) -> Option<Self> {
let lines: Vec<&str> = text.lines().collect();
if lines.len() < 2 {
return None;
}
let parse_row = |line: &str| -> Vec<String> {
let trimmed = line.trim();
let inner = trimmed.strip_prefix('|').unwrap_or(trimmed);
let inner = inner.strip_suffix('|').unwrap_or(inner);
inner.split('|').map(|c| c.trim().to_string()).collect()
};
let is_separator = |line: &str| -> bool {
let trimmed = line.trim();
let inner = trimmed.strip_prefix('|').unwrap_or(trimmed);
let inner = inner.strip_suffix('|').unwrap_or(inner);
inner.split('|').all(|seg| {
let s = seg.trim();
s.chars().all(|c| c == '-' || c == ':') && s.len() >= 1
})
};
let header = parse_row(lines[0]);
if !is_separator(lines[1]) {
return None;
}
let mut rows = vec![header];
for line in &lines[2..] {
let trimmed = line.trim();
if trimmed.is_empty() {
break;
}
rows.push(parse_row(trimmed));
}
let col_count = rows.iter().map(|r| r.len()).max().unwrap_or(0);
for row in &mut rows {
while row.len() < col_count {
row.push(String::new());
}
}
let widths: Vec<f32> = (0..col_count)
.map(|ci| {
let max_len = rows.iter()
.map(|r| r.get(ci).map(|s| s.len()).unwrap_or(0))
.max()
.unwrap_or(0);
((max_len as f32) * 9.0).max(MIN_COL_WIDTH).min(300.0)
})
.collect();
Some(Self::new(rows, Some(widths)))
}
pub fn from_eval_rows(data: Vec<Vec<String>>) -> Self {
let col_count = data.iter().map(|r| r.len()).max().unwrap_or(0);
let widths: Vec<f32> = (0..col_count)
.map(|ci| {
let max_len = data.iter()
.map(|r| r.get(ci).map(|s| s.len()).unwrap_or(0))
.max()
.unwrap_or(0);
((max_len as f32) * 9.0).max(MIN_COL_WIDTH).min(300.0)
})
.collect();
Self::new(data, Some(widths))
}
pub fn col_count(&self) -> usize {
self.col_widths.len()
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn update(&mut self, msg: TableMessage) {
match msg {
TableMessage::CellChanged(row, col, val) => {
if row < self.rows.len() && col < self.rows[row].len() {
self.rows[row][col] = val;
}
}
TableMessage::FocusCell(row, col) => {
self.focused_cell = Some((row, col));
}
TableMessage::AddRow => {
let cols = self.col_count();
self.rows.push(vec![String::new(); cols]);
}
TableMessage::AddColumn => {
let new_width = DEFAULT_COL_WIDTH;
self.col_widths.push(new_width);
for row in &mut self.rows {
row.push(String::new());
}
}
}
}
pub fn next_cell(&self, row: usize, col: usize) -> Option<(usize, usize)> {
let cols = self.col_count();
let rows = self.row_count();
if col + 1 < cols {
Some((row, col + 1))
} else if row + 1 < rows {
Some((row + 1, 0))
} else {
None
}
}
pub fn prev_cell(&self, row: usize, col: usize) -> Option<(usize, usize)> {
let cols = self.col_count();
if col > 0 {
Some((row, col - 1))
} else if row > 0 {
Some((row - 1, cols.saturating_sub(1)))
} else {
None
}
}
pub fn cell_below(&self, row: usize, col: usize) -> Option<(usize, usize)> {
if row + 1 < self.row_count() {
Some((row + 1, col))
} else {
None
}
}
pub fn to_markdown(&self) -> String {
if self.rows.is_empty() {
return String::new();
}
let col_count = self.col_count();
let mut widths = vec![0usize; col_count];
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
if i < col_count {
widths[i] = widths[i].max(cell.len()).max(3);
}
}
}
let mut out = String::new();
// header
out.push('|');
if let Some(header) = self.rows.first() {
for (i, cell) in header.iter().enumerate() {
let w = if i < widths.len() { widths[i] } else { 3 };
out.push_str(&format!(" {:width$} |", cell, width = w));
}
}
out.push('\n');
// separator
out.push('|');
for w in &widths {
out.push_str(&format!("-{}-|", "-".repeat(*w)));
}
out.push('\n');
// data rows
for row in self.rows.iter().skip(1) {
out.push('|');
for (i, cell) in row.iter().enumerate() {
let w = if i < widths.len() { widths[i] } else { 3 };
out.push_str(&format!(" {:width$} |", cell, width = w));
}
out.push('\n');
}
out
}
pub fn pending_focus_id(&self) -> Option<WidgetId> {
self.focused_cell
.map(|(r, c)| WidgetId::from(format!("table_cell_{}_{}", r, c)))
}
}
fn cell_id(row: usize, col: usize) -> WidgetId {
WidgetId::from(format!("table_cell_{}_{}", row, col))
}
fn cell_input_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
let p = palette::current();
text_input::Style {
background: Background::Color(p.surface0),
border: Border {
color: p.surface2,
width: 1.0,
radius: 0.0.into(),
},
icon: p.overlay2,
placeholder: p.overlay0,
value: p.text,
selection: Color { a: 0.4, ..p.blue },
}
}
fn header_cell_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
let p = palette::current();
text_input::Style {
background: Background::Color(p.mantle),
border: Border {
color: p.surface2,
width: 1.0,
radius: 0.0.into(),
},
icon: p.overlay2,
placeholder: p.overlay0,
value: p.text,
selection: Color { a: 0.4, ..p.blue },
}
}
pub fn table_view<'a, Message, F>(
state: &'a TableState,
on_msg: F,
) -> Element<'a, Message, Theme, iced_wgpu::Renderer>
where
Message: Clone + 'a,
F: Fn(TableMessage) -> Message + 'a + Copy,
{
let mut col_elements: Vec<Element<'a, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
for (ri, row) in state.rows.iter().enumerate() {
let is_header = ri == 0;
let mut row_cells: Vec<Element<'a, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
for (ci, cell) in row.iter().enumerate() {
let width = state.col_widths.get(ci).copied().unwrap_or(DEFAULT_COL_WIDTH);
let r = ri;
let c = ci;
let style_fn: fn(&Theme, text_input::Status) -> text_input::Style = if is_header {
header_cell_style
} else {
cell_input_style
};
let font = if is_header {
Font { weight: iced_wgpu::core::font::Weight::Bold, ..Font::MONOSPACE }
} else {
Font::MONOSPACE
};
let input = text_input::TextInput::new("", cell)
.on_input(move |val| on_msg(TableMessage::CellChanged(r, c, val)))
.id(cell_id(ri, ci))
.font(font)
.size(13.0)
.padding(CELL_PADDING)
.width(Length::Fixed(width))
.style(style_fn);
row_cells.push(input.into());
}
let row_el: Element<'a, Message, Theme, iced_wgpu::Renderer> =
iced_widget::row(row_cells).spacing(0.0).into();
col_elements.push(row_el);
}
let table: Element<'a, Message, Theme, iced_wgpu::Renderer> =
iced_widget::column(col_elements).spacing(0.0).into();
iced_widget::container(table)
.padding(Padding { top: 4.0, right: 0.0, bottom: 4.0, left: 8.0 })
.style(|_theme: &Theme| container::Style {
background: None,
border: Border::default(),
text_color: None,
shadow: Shadow::default(),
snap: false,
})
.into()
}

View File

@ -1,198 +0,0 @@
use iced_wgpu::core::text::LineHeight;
use iced_wgpu::core::{
alignment, Element, Font, Length, Pixels, Point, Rectangle, Theme,
};
use iced_widget::canvas;
use crate::palette;
const NODE_HEIGHT: f32 = 20.0;
const INDENT_PX: f32 = 20.0;
const FONT_SIZE: f32 = 13.0;
const BRANCH_INSET: f32 = 12.0;
#[derive(Debug, Clone)]
pub enum TreeMessage {}
struct TreeNode {
label: String,
depth: usize,
is_last: bool,
}
fn flatten_tree(val: &serde_json::Value, depth: usize, is_last: bool, out: &mut Vec<TreeNode>) {
match val {
serde_json::Value::Array(items) => {
if depth > 0 {
out.push(TreeNode {
label: "[array]".into(),
depth,
is_last,
});
}
let len = items.len();
for (i, item) in items.iter().enumerate() {
flatten_tree(item, depth + 1, i == len - 1, out);
}
}
serde_json::Value::Object(_) => {
out.push(TreeNode {
label: "{object}".into(),
depth,
is_last,
});
}
serde_json::Value::String(s) => {
out.push(TreeNode {
label: format!("\"{}\"", s),
depth,
is_last,
});
}
serde_json::Value::Number(n) => {
out.push(TreeNode {
label: n.to_string(),
depth,
is_last,
});
}
serde_json::Value::Bool(b) => {
out.push(TreeNode {
label: b.to_string(),
depth,
is_last,
});
}
serde_json::Value::Null => {
out.push(TreeNode {
label: "null".into(),
depth,
is_last,
});
}
}
}
pub struct TreeProgram {
nodes: Vec<TreeNode>,
total_height: f32,
}
impl TreeProgram {
pub fn from_json(val: &serde_json::Value) -> Self {
let mut nodes = Vec::new();
match val {
serde_json::Value::Array(items) => {
let len = items.len();
for (i, item) in items.iter().enumerate() {
flatten_tree(item, 0, i == len - 1, &mut nodes);
}
}
_ => {
flatten_tree(val, 0, true, &mut nodes);
}
}
let total_height = (nodes.len() as f32 * NODE_HEIGHT).max(NODE_HEIGHT);
Self { nodes, total_height }
}
pub fn height(&self) -> f32 {
self.total_height
}
}
impl<Message: Clone> canvas::Program<Message, Theme, iced_wgpu::Renderer> for TreeProgram {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced_wgpu::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: iced_wgpu::core::mouse::Cursor,
) -> Vec<canvas::Geometry<iced_wgpu::Renderer>> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let p = palette::current();
let connector_color = p.surface2;
let label_color = p.text;
let array_color = p.overlay1;
for (i, node) in self.nodes.iter().enumerate() {
let y = i as f32 * NODE_HEIGHT;
let indent_x = node.depth as f32 * INDENT_PX + 8.0;
// vertical connector from parent
if node.depth > 0 {
let parent_x = (node.depth - 1) as f32 * INDENT_PX + 8.0;
let connector = canvas::Path::new(|b| {
b.move_to(Point::new(parent_x, y));
b.line_to(Point::new(parent_x, y + NODE_HEIGHT / 2.0));
b.line_to(Point::new(parent_x + BRANCH_INSET, y + NODE_HEIGHT / 2.0));
});
frame.stroke(
&connector,
canvas::Stroke::default()
.with_width(1.0)
.with_color(connector_color),
);
// extend vertical line upward to connect siblings
if !node.is_last {
let vert = canvas::Path::line(
Point::new(indent_x - INDENT_PX, y + NODE_HEIGHT / 2.0),
Point::new(indent_x - INDENT_PX, y + NODE_HEIGHT),
);
frame.stroke(
&vert,
canvas::Stroke::default()
.with_width(1.0)
.with_color(connector_color),
);
}
}
let text_color = if node.label.starts_with('[') || node.label.starts_with('{') {
array_color
} else {
label_color
};
let branch_char = if node.depth == 0 {
String::new()
} else if node.is_last {
"\u{2514}\u{2500} ".into() // └─
} else {
"\u{251C}\u{2500} ".into() // ├─
};
let display = format!("{}{}", branch_char, node.label);
frame.fill_text(canvas::Text {
content: display,
position: Point::new(indent_x, y + 2.0),
max_width: bounds.width - indent_x,
color: text_color,
size: Pixels(FONT_SIZE),
line_height: LineHeight::Relative(1.3),
font: Font::MONOSPACE,
align_x: alignment::Horizontal::Left.into(),
align_y: alignment::Vertical::Top,
shaping: iced_wgpu::core::text::Shaping::Basic,
});
}
vec![frame.into_geometry()]
}
}
pub fn view<'a, Message: Clone + 'a>(
data: &serde_json::Value,
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
let program = TreeProgram::from_json(data);
let h = program.height();
canvas::Canvas::new(program)
.width(Length::Fill)
.height(Length::Fixed(h))
.into()
}