Extended for easier embedding.

This commit is contained in:
jess 2026-05-27 22:11:59 -07:00
parent 23b7b3784b
commit 9b5dbbdf2b
11 changed files with 456 additions and 215 deletions

View File

@ -18,6 +18,7 @@ pub fn highlight_preview(source: &str) -> Vec<PreviewLine> {
lang: "rust".to_string(),
source: source.to_string(),
user_idents: crate::syntax::scan_user_idents_in(source),
rules: crate::syntax::SyntaxRules::cordial(),
};
let mut highlighter = SyntaxHighlighter::new(&settings);

View File

@ -406,6 +406,16 @@ impl super::EditorState {
self.content_mut().jump_to_line(target);
}
/// scrolls the viewport and places the cursor at the given line.
pub(super) fn jump_to_line(&mut self, line: usize) {
let clamped = line.min(self.content().line_count().saturating_sub(1));
self.content_mut().jump_to_line(clamped);
self.safe_move_to(crate::text_widget::Cursor {
position: crate::text_widget::Position { line: clamped, column: 0 },
selection: None,
});
}
/// builds the clipboard payload from the focused table
pub(super) fn copy_focused_table_selection(&self) -> Option<String> {
let block = self.block_at(self.focused_block)?;

View File

@ -246,6 +246,7 @@ impl EditorState {
lang: lang_for_block.clone(),
source: tb.content.text(),
user_idents: self.cached_user_idents.clone(),
rules: self.syntax_rules.clone(),
};
let editor_el: Element<'_, Message, Theme, iced_wgpu::Renderer> = editor
.highlight_with::<SyntaxHighlighter>(
@ -381,32 +382,24 @@ impl EditorState {
}
}
/// builds the right-edge minimap as a hover-aware overlay.
/// builds the right-edge minimap as a clickable AST declaration list.
fn minimap_overlay(&self) -> Option<Element<'_, Message, Theme, iced_wgpu::Renderer>> {
if !self.minimap_enabled { return None; }
if self.render_mode != RenderMode::Editor { return None; }
if self.cached_minimap_lines.is_empty() { return None; }
let lines = self.cached_minimap_lines.clone();
let scroll = self.content().scroll_line();
let line_h = self.line_height().max(1.0);
let visible_lines = (self.viewport_size.1 / line_h).max(1.0) as usize;
let suppressed = self.minimap_hover_only && !self.minimap_hovered;
let data = crate::minimap::MinimapData {
lines,
viewport_first: scroll,
viewport_last: scroll.saturating_add(visible_lines),
entries: self.cached_minimap_lines.clone(),
hovered: self.minimap_hovered,
suppressed,
};
let strip_w = self.font_size * 6.0;
let canvas = crate::minimap::minimap(data, strip_w, Message::MinimapJump);
let strip_w = self.font_size * 12.0;
let map = crate::minimap::minimap(data, strip_w, self.font_size, Message::MinimapJump);
let hover_zone = iced_widget::mouse_area(canvas)
let hover_zone = iced_widget::mouse_area(map)
.on_enter(Message::MinimapHover(true))
.on_exit(Message::MinimapHover(false));
@ -720,7 +713,8 @@ impl EditorState {
let settings = SyntaxSettings {
lang: lang_for_block,
source: tb.content.text(),
user_idents: syntax::scan_user_idents_in(&self.full_text()),
user_idents: self.cached_user_idents.clone(),
rules: self.syntax_rules.clone(),
};
editor
.highlight_with::<SyntaxHighlighter>(

View File

@ -109,6 +109,8 @@ pub struct EditorState {
pub(super) cached_user_idents: HashMap<String, u8>,
/// cached minimap line data, recomputed on text change only.
pub(super) cached_minimap_lines: Vec<crate::minimap::MinimapLine>,
/// custom keyword/builtin/type table layered on top of Cordial.
pub syntax_rules: crate::syntax::SyntaxRules,
}
impl EditorState {
@ -175,9 +177,15 @@ impl EditorState {
snapping: false,
cached_user_idents: HashMap::new(),
cached_minimap_lines: Vec::new(),
syntax_rules: crate::syntax::SyntaxRules::cordial(),
}
}
/// replaces the active syntax rule set.
pub fn set_syntax_rules(&mut self, rules: crate::syntax::SyntaxRules) {
self.syntax_rules = rules;
}
/// recomputes cached syntax idents and minimap lines from current document text.
pub(super) fn refresh_text_caches(&mut self) {
let text = self.full_text();

View File

@ -160,7 +160,7 @@ pub enum Message {
/// reports pointer hover state over the minimap.
MinimapHover(bool),
/// minimap click at y-fraction 0.0..1.0.
MinimapJump(f32),
MinimapJump(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@ -929,8 +929,8 @@ impl super::EditorState {
Message::MinimapHover(over) => {
self.minimap_hovered = over;
}
Message::MinimapJump(frac) => {
self.jump_to_fraction(frac);
Message::MinimapJump(line) => {
self.jump_to_line(line);
}
}
}

71
viewport/src/embed.rs Normal file
View File

@ -0,0 +1,71 @@
//! helpers for embedding EditorState inside an external iced application.
//!
//! the editor produces a fully-functional iced widget via EditorState::view().
//! callers also need to drive periodic state work (eval debounce, autosave hints)
//! by calling EditorState::tick() once per frame, and drain pending clipboard or
//! shell-action output. these helpers wrap the common patterns.
//!
//! example:
//!
//! ```ignore
//! use acord_viewport::{EditorState, embed};
//!
//! struct App { editor: EditorState }
//!
//! enum Msg { Acord(acord_viewport::Message), Tick }
//!
//! fn update(&mut self, msg: Msg) {
//! match msg {
//! Msg::Acord(m) => self.editor.update(m),
//! Msg::Tick => {
//! self.editor.tick();
//! let pending = self.editor.drain_pending();
//! if let Some(text) = pending.clipboard {
//! // copy to host clipboard
//! }
//! }
//! }
//! }
//!
//! fn view(&self) -> Element<'_, Msg> {
//! self.editor.view().map(Msg::Acord)
//! }
//!
//! fn subscription(&self) -> Subscription<Msg> {
//! iced::time::every(embed::TICK_INTERVAL).map(|_| Msg::Tick)
//! }
//! ```
use std::time::Duration;
use crate::editor::EditorState;
/// recommended tick interval for embedded use (60 fps cadence).
pub const TICK_INTERVAL: Duration = Duration::from_millis(16);
/// snapshot of host-handled state the editor produced this frame.
pub struct Pending {
/// text the editor wants written to the host clipboard, if any.
pub clipboard: Option<String>,
/// numeric command the host shell should act on, if any.
pub shell_action: Option<crate::editor::ShellAction>,
/// widget that should receive iced focus this frame, if any.
pub focus: Option<iced_wgpu::core::widget::Id>,
}
impl EditorState {
/// pulls every host-handled output for this frame and clears it from the editor.
pub fn drain_pending(&mut self) -> Pending {
Pending {
clipboard: self.pending_clipboard.take(),
shell_action: self.take_pending_shell_action(),
focus: self.take_pending_focus(),
}
}
/// records the surface size so the minimap, scrollable math, and free-layer
/// placement can size themselves correctly.
pub fn set_viewport_size(&mut self, width: f32, height: f32) {
self.viewport_size = (width, height);
}
}

View File

@ -35,6 +35,7 @@ pub mod text_block;
pub mod text_widget;
pub mod tree_block;
pub mod widgets;
pub mod embed;
pub use acord_core::*;
@ -43,7 +44,7 @@ pub use crate::block::{Block, BlockCommand, LayeredView, ViewCtx};
pub use crate::editor::{EditorState, Message, RenderMode, ShellAction};
pub use crate::palette::{Palette, current as current_palette, set_theme as set_palette_theme};
pub use crate::selection::{BlockId, InnerPath, NodePath, Selection, TextPos};
pub use crate::syntax::{SyntaxHighlight, SyntaxHighlighter, SyntaxSettings, EDITOR_FONT};
pub use crate::syntax::{SyntaxHighlight, SyntaxHighlighter, SyntaxRules, SyntaxSettings, EDITOR_FONT};
pub use crate::text_widget::{Content, TextEditor};
use iced_graphics::Viewport;

View File

@ -1,188 +1,277 @@
use iced_wgpu::core::{
mouse, Color, Element, Length, Point, Rectangle, Size, Theme,
alignment, mouse, Color, Element, Font, Length, Theme,
};
use iced_widget::canvas::{self, Frame};
use iced_widget::{column, container, mouse_area, text};
use crate::palette;
#[derive(Clone, Copy, Debug)]
pub enum LineKind {
Empty,
Plain,
Heading,
Code,
List,
Quote,
}
use crate::syntax;
#[derive(Clone, Debug)]
pub struct MinimapLine {
pub width_chars: u16,
pub kind: LineKind,
pub struct MapEntry {
pub kind: DeclKind,
pub name: String,
pub trait_group: Option<String>,
pub line: usize,
}
/// classifies a single source line by minimap colour category.
pub fn classify(line: &str) -> MinimapLine {
let trimmed = line.trim_start();
let kind = if trimmed.is_empty() {
LineKind::Empty
} else if trimmed.starts_with('#') {
LineKind::Heading
} else if trimmed.starts_with("```") || trimmed.starts_with(" ") || trimmed.starts_with('\t') {
LineKind::Code
} else if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
LineKind::List
} else if trimmed.starts_with("> ") {
LineKind::Quote
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DeclKind {
Fn,
Impl,
Trait,
Struct,
Let,
Heading,
Mod,
Enum,
}
impl DeclKind {
pub fn label(&self) -> &'static str {
match self {
DeclKind::Fn => "fn",
DeclKind::Impl => "impl",
DeclKind::Trait => "trait",
DeclKind::Struct => "struct",
DeclKind::Let => "let",
DeclKind::Heading => "#",
DeclKind::Mod => "mod",
DeclKind::Enum => "enum",
}
}
}
/// extracts named declarations from cordial/markdown source.
pub fn extract_declarations(source: &str) -> Vec<MapEntry> {
let mut entries = Vec::new();
for (line_idx, line) in source.lines().enumerate() {
let trimmed = line.trim_start();
if let Some(entry) = classify_decl(trimmed, line_idx) {
entries.push(entry);
}
}
entries
}
fn classify_decl(trimmed: &str, line: usize) -> Option<MapEntry> {
let stripped = strip_visibility(trimmed);
if let Some(rest) = stripped.strip_prefix("fn ") {
let name = ident_at_start(rest)?;
return Some(MapEntry { kind: DeclKind::Fn, name, trait_group: None, line });
}
if let Some(rest) = stripped.strip_prefix("async fn ") {
let name = ident_at_start(rest)?;
return Some(MapEntry { kind: DeclKind::Fn, name, trait_group: None, line });
}
if let Some(rest) = stripped.strip_prefix("unsafe fn ") {
let name = ident_at_start(rest)?;
return Some(MapEntry { kind: DeclKind::Fn, name, trait_group: None, line });
}
if let Some(rest) = stripped.strip_prefix("impl ") {
return Some(parse_impl_entry(rest, line));
}
if let Some(rest) = stripped.strip_prefix("trait ") {
let name = ident_at_start(rest)?;
let group = name.clone();
return Some(MapEntry { kind: DeclKind::Trait, name, trait_group: Some(group), line });
}
if let Some(rest) = stripped.strip_prefix("struct ") {
let name = ident_at_start(rest)?;
return Some(MapEntry { kind: DeclKind::Struct, name, trait_group: None, line });
}
if let Some(rest) = stripped.strip_prefix("enum ") {
let name = ident_at_start(rest)?;
return Some(MapEntry { kind: DeclKind::Enum, name, trait_group: None, line });
}
if let Some(rest) = stripped.strip_prefix("mod ") {
let name = ident_at_start(rest)?;
return Some(MapEntry { kind: DeclKind::Mod, name, trait_group: None, line });
}
if let Some(rest) = stripped.strip_prefix("type ") {
let name = ident_at_start(rest)?;
return Some(MapEntry { kind: DeclKind::Struct, name, trait_group: None, line });
}
if let Some(rest) = stripped.strip_prefix("const ") {
let name = ident_at_start(rest)?;
return Some(MapEntry { kind: DeclKind::Let, name, trait_group: None, line });
}
if let Some(rest) = stripped.strip_prefix("static ") {
let name = ident_at_start(rest)?;
return Some(MapEntry { kind: DeclKind::Let, name, trait_group: None, line });
}
if let Some(rest) = trimmed.strip_prefix("let ") {
let name = ident_at_start(rest)?;
if rest.contains('=') {
return Some(MapEntry { kind: DeclKind::Let, name, trait_group: None, line });
}
}
if trimmed.starts_with('#') && !trimmed.starts_with("#!") {
let level = trimmed.chars().take_while(|c| *c == '#').count();
let text = trimmed[level..].trim();
if !text.is_empty() {
return Some(MapEntry {
kind: DeclKind::Heading,
name: text.to_string(),
trait_group: None,
line,
});
}
}
None
}
fn parse_impl_entry(rest: &str, line: usize) -> MapEntry {
let words: Vec<&str> = rest.split_whitespace().collect();
if words.len() >= 3 && words[1] == "for" {
let trait_name = words[0].to_string();
let type_name = words[2].trim_matches('{').to_string();
MapEntry {
kind: DeclKind::Impl,
name: format!("{} for {}", trait_name, type_name),
trait_group: Some(trait_name),
line,
}
} else {
LineKind::Plain
};
let width = line.chars().count().min(u16::MAX as usize) as u16;
MinimapLine { width_chars: width, kind }
let type_name = words.first().map(|w| w.trim_matches('{')).unwrap_or("").to_string();
MapEntry {
kind: DeclKind::Impl,
name: type_name,
trait_group: None,
line,
}
}
}
/// turns the document text into per-line minimap data in one pass.
pub fn classify_text(text: &str) -> Vec<MinimapLine> {
text.lines().map(classify).collect()
/// strips `pub`, `pub(crate)`, `pub(super)`, `pub(in path)` from the front.
fn strip_visibility(s: &str) -> &str {
if let Some(rest) = s.strip_prefix("pub(") {
if let Some(end) = rest.find(')') {
let after = &rest[end + 1..];
return after.trim_start();
}
}
if let Some(rest) = s.strip_prefix("pub ") {
return rest.trim_start();
}
s
}
fn ident_at_start(s: &str) -> Option<String> {
let s = s.trim_start();
let end = s.find(|c: char| !c.is_alphanumeric() && c != '_').unwrap_or(s.len());
if end == 0 { return None; }
Some(s[..end].to_string())
}
/// assigns rainbow slots to trait groups.
fn trait_color_map(entries: &[MapEntry]) -> std::collections::HashMap<String, u8> {
let mut map = std::collections::HashMap::new();
let mut next: u32 = 0;
for e in entries {
if let Some(ref group) = e.trait_group {
if !map.contains_key(group) {
let slot = ((next * syntax::USER_IDENT_HOP as u32)
% syntax::USER_IDENT_PALETTE_SIZE as u32) as u8;
map.insert(group.clone(), slot);
next += 1;
}
}
}
map
}
fn entry_color(entry: &MapEntry, color_map: &std::collections::HashMap<String, u8>) -> Color {
let p = palette::current();
if let Some(ref group) = entry.trait_group {
if let Some(&slot) = color_map.get(group) {
return syntax::rainbow_color(slot as u32);
}
}
match entry.kind {
DeclKind::Fn => p.blue,
DeclKind::Impl => p.mauve,
DeclKind::Trait => p.green,
DeclKind::Struct => p.peach,
DeclKind::Enum => p.yellow,
DeclKind::Let => p.overlay1,
DeclKind::Heading => p.text,
DeclKind::Mod => p.teal,
}
}
#[derive(Clone)]
pub struct MinimapData {
pub lines: Vec<MinimapLine>,
pub viewport_first: usize,
pub viewport_last: usize,
pub entries: Vec<MapEntry>,
pub hovered: bool,
pub suppressed: bool,
}
struct MinimapProgram<M, F>
where
M: Clone + 'static,
F: Fn(f32) -> M,
{
data: MinimapData,
on_jump: F,
_marker: std::marker::PhantomData<fn() -> M>,
}
impl<M, F> canvas::Program<M, Theme, iced_wgpu::Renderer> for MinimapProgram<M, F>
where
M: Clone + 'static,
F: Fn(f32) -> M,
{
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 = Frame::new(renderer, bounds.size());
let p = palette::current();
let total = self.data.lines.len().max(1) as f32;
let h = bounds.height;
let pixels_per_line = (h / total).max(0.5);
let bar_h = pixels_per_line.max(1.0);
let alpha = if self.data.suppressed {
0.0
} else if self.data.hovered {
0.55
} else {
0.18
};
if alpha == 0.0 {
return vec![frame.into_geometry()];
}
let max_chars = self.data.lines.iter().map(|l| l.width_chars as f32).fold(1.0, f32::max);
for (i, line) in self.data.lines.iter().enumerate() {
let y = (i as f32 / total) * h;
let bar_w = (line.width_chars as f32 / max_chars) * bounds.width * 0.85;
let color = match line.kind {
LineKind::Empty => continue,
LineKind::Heading => Color { a: alpha + 0.20, ..p.mauve },
LineKind::Code => Color { a: alpha + 0.05, ..p.peach },
LineKind::List => Color { a: alpha + 0.05, ..p.green },
LineKind::Quote => Color { a: alpha + 0.05, ..p.teal },
LineKind::Plain => Color { a: alpha, ..p.text },
};
frame.fill_rectangle(
Point::new(bounds.width * 0.075, y),
Size::new(bar_w, bar_h),
color,
);
}
if self.data.viewport_last > self.data.viewport_first {
let top = (self.data.viewport_first as f32 / total) * h;
let bot = ((self.data.viewport_last as f32) / total) * h;
let height = (bot - top).max(8.0);
let indicator_alpha = if self.data.hovered { 0.22 } else { 0.12 };
frame.fill_rectangle(
Point::new(0.0, top),
Size::new(bounds.width, height),
Color { a: indicator_alpha, ..p.text },
);
}
vec![frame.into_geometry()]
}
fn update(
&self,
_state: &mut (),
event: &canvas::Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<M>> {
if self.data.suppressed { return None; }
let pos = cursor.position_in(bounds)?;
match event {
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let frac = (pos.y / bounds.height).clamp(0.0, 1.0);
Some(canvas::Action::publish((self.on_jump)(frac)).and_capture())
}
_ => None,
}
}
fn mouse_interaction(
&self,
_state: &(),
bounds: Rectangle,
cursor: mouse::Cursor,
) -> mouse::Interaction {
if !self.data.suppressed && cursor.is_over(bounds) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
}
/// builds a minimap canvas pinned at a fixed width.
pub fn minimap<'a, M, F>(
/// builds the minimap as a clickable declaration list.
pub fn minimap<'a, M>(
data: MinimapData,
width: f32,
on_jump: F,
font_size: f32,
on_jump: impl Fn(usize) -> M + 'a,
) -> Element<'a, M, Theme, iced_wgpu::Renderer>
where
M: Clone + 'static,
F: Fn(f32) -> M + 'a,
{
canvas::Canvas::new(MinimapProgram::<M, F> {
data,
on_jump,
_marker: std::marker::PhantomData,
})
let p = palette::current();
let color_map = trait_color_map(&data.entries);
let alpha = if data.suppressed { 0.0 } else if data.hovered { 1.0 } else { 0.35 };
if alpha == 0.0 || data.entries.is_empty() {
return container(text(""))
.width(Length::Fixed(width))
.height(Length::Fill)
.into();
}
let sz = (font_size * 0.7).max(8.0);
let mut rows: Vec<Element<'a, M, Theme, iced_wgpu::Renderer>> = Vec::new();
let _ = p;
for entry in &data.entries {
let color = entry_color(entry, &color_map);
let dimmed = Color { a: alpha, ..color };
let kind_label = entry.kind.label();
let display = format!("{} {}", kind_label, entry.name);
let display = if display.len() > 24 {
format!("{}...", &display[..21])
} else {
display
};
let line = entry.line;
let row_el: Element<'a, M, Theme, iced_wgpu::Renderer> = mouse_area(
text(display)
.font(Font::MONOSPACE)
.size(sz)
.color(dimmed)
)
.on_press(on_jump(line))
.into();
rows.push(row_el);
}
let col = column(rows)
.spacing(1.0)
.width(Length::Fixed(width));
container(
iced_widget::scrollable(col).height(Length::Fill)
)
.width(Length::Fixed(width))
.height(Length::Fill)
.into()
}
// keep for backward compat with the caching system in EditorState
pub type MinimapLine = MapEntry;
pub fn classify_text(text: &str) -> Vec<MinimapLine> {
extract_declarations(text)
}

View File

@ -76,6 +76,73 @@ pub struct SyntaxSettings {
pub source: String,
/// doc-wide user-ident to rainbow-slot map, computed across all text blocks.
pub user_idents: HashMap<String, u8>,
/// optional extra keywords/builtins layered on top of Cordial.
pub rules: SyntaxRules,
}
/// extensible token table for languages built on top of Cordial.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SyntaxRules {
pub extra_keywords: std::collections::BTreeSet<String>,
pub extra_builtins: std::collections::BTreeSet<String>,
pub extra_types: std::collections::BTreeSet<String>,
/// when true, the hardcoded Cordial keywords/builtins remain active.
pub include_cordial: bool,
}
impl SyntaxRules {
/// fresh table with the Cordial defaults active.
pub fn cordial() -> Self {
Self {
extra_keywords: Default::default(),
extra_builtins: Default::default(),
extra_types: Default::default(),
include_cordial: true,
}
}
/// fresh table with no defaults at all.
pub fn empty() -> Self {
Self {
extra_keywords: Default::default(),
extra_builtins: Default::default(),
extra_types: Default::default(),
include_cordial: false,
}
}
pub fn keyword(mut self, w: impl Into<String>) -> Self {
self.extra_keywords.insert(w.into());
self
}
pub fn builtin(mut self, w: impl Into<String>) -> Self {
self.extra_builtins.insert(w.into());
self
}
pub fn ty(mut self, w: impl Into<String>) -> Self {
self.extra_types.insert(w.into());
self
}
pub fn keywords<I, S>(mut self, items: I) -> Self
where I: IntoIterator<Item = S>, S: Into<String>
{
self.extra_keywords.extend(items.into_iter().map(Into::into));
self
}
pub fn builtins<I, S>(mut self, items: I) -> Self
where I: IntoIterator<Item = S>, S: Into<String>
{
self.extra_builtins.extend(items.into_iter().map(Into::into));
self
}
}
impl Default for SyntaxRules {
fn default() -> Self { Self::cordial() }
}
#[derive(Clone, Copy, Debug)]
@ -103,57 +170,49 @@ pub struct SyntaxHighlighter {
user_idents: HashMap<String, u8>,
/// per-line tree-sitter spans for fenced code body lines, keyed by absolute line index.
code_block_spans: HashMap<usize, Vec<(Range<usize>, SyntaxHighlight)>>,
/// line count at last full rebuild, drives incremental skip.
prev_line_count: usize,
rules: SyntaxRules,
}
impl SyntaxHighlighter {
fn rebuild(&mut self, source: &str) {
let new_line_count = source.split('\n').count();
let structure_changed = new_line_count != self.prev_line_count;
self.spans = highlight_source(source, &self.lang);
self.line_offsets.clear();
let mut offset = 0;
for line in source.split('\n') {
self.line_offsets.push(offset);
offset += line.len() + 1;
}
let classified = classify_document(source);
self.line_kinds = classified.into_iter().map(|cl| cl.kind).collect();
if structure_changed {
self.spans = highlight_source(source, &self.lang);
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);
}
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 {
if in_fence { in_fence = false; }
self.line_decors.push(LineDecor::None);
}
} else {
if in_fence { in_fence = false; }
self.line_decors.push(LineDecor::None);
}
self.scan_fenced_code_blocks(source);
self.prev_line_count = new_line_count;
}
self.in_fenced_code = false;
self.current_line = 0;
self.scan_fenced_code_blocks(source);
}
/// highlights language-tagged fenced blocks via tree-sitter, stashing per-line spans.
@ -437,7 +496,7 @@ fn extract_paren_idents(s: &str, map: &mut HashMap<String, u8>, slot: &mut u32)
}
}
fn highlight_cordial(line: &str, user_idents: &HashMap<String, u8>) -> Vec<(Range<usize>, SyntaxHighlight)> {
fn highlight_cordial(line: &str, user_idents: &HashMap<String, u8>, rules: &SyntaxRules) -> Vec<(Range<usize>, SyntaxHighlight)> {
let bytes = line.as_bytes();
let len = bytes.len();
let mut spans: Vec<(Range<usize>, SyntaxHighlight)> = Vec::new();
@ -552,11 +611,17 @@ fn highlight_cordial(line: &str, user_idents: &HashMap<String, u8>) -> Vec<(Rang
let start = i;
while i < len && is_ident_byte(bytes[i]) { i += 1; }
let word = &line[start..i];
if is_cordial_keyword(word) {
let is_kw = (rules.include_cordial && is_cordial_keyword(word))
|| rules.extra_keywords.contains(word);
let is_bi = (rules.include_cordial && is_cordial_builtin(word))
|| rules.extra_builtins.contains(word);
let is_ty = (rules.include_cordial && is_cordial_type_annotation(word))
|| rules.extra_types.contains(word);
if is_kw {
spans.push((start..i, SyntaxHighlight { kind: COR_KEYWORD }));
} else if is_cordial_builtin(word) {
} else if is_bi {
spans.push((start..i, SyntaxHighlight { kind: COR_BUILTIN_FN }));
} else if is_cordial_type_annotation(word) && last_token_is_colon(&spans) {
} else if is_ty && last_token_is_colon(&spans) {
spans.push((start..i, SyntaxHighlight { kind: COR_TYPE_ANN }));
} else if let Some(&slot) = user_idents.get(word) {
spans.push((start..i, SyntaxHighlight { kind: USER_IDENT_BASE + slot }));
@ -903,7 +968,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
line_decors: Vec::new(),
user_idents: settings.user_idents.clone(),
code_block_spans: HashMap::new(),
prev_line_count: 0,
rules: settings.rules.clone(),
};
h.rebuild(&settings.source);
h
@ -912,6 +977,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
fn update(&mut self, new_settings: &Self::Settings) {
self.lang = new_settings.lang.clone();
self.user_idents = new_settings.user_idents.clone();
self.rules = new_settings.rules.clone();
self.rebuild(&new_settings.source);
}
@ -942,7 +1008,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
&& matches!(self.line_kinds[ln], LineKind::Cordial | LineKind::Eval | LineKind::Comment)
{
if self.in_fenced_code { self.in_fenced_code = false; }
return highlight_cordial(line, &self.user_idents).into_iter();
return highlight_cordial(line, &self.user_idents, &self.rules).into_iter();
}
let is_markdown = !is_pure_code

View File

@ -92,6 +92,7 @@ impl<Message: Clone + 'static> Block<Message> for TextBlock {
let settings = SyntaxSettings {
lang: self.lang.clone(),
user_idents: syntax::scan_user_idents_in(&source),
rules: syntax::SyntaxRules::cordial(),
source,
};
let editor_el: Element<'a, Message, Theme, iced_wgpu::Renderer> = editor