Extended for easier embedding.
This commit is contained in:
parent
23b7b3784b
commit
9b5dbbdf2b
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue