syntec, but not compiling

This commit is contained in:
Priec
2025-08-19 00:46:11 +02:00
parent a3f578ebac
commit 3fdb7e4e37
9 changed files with 1262 additions and 27 deletions

View File

@@ -25,6 +25,7 @@ async-trait.workspace = true
regex = { workspace = true, optional = true }
ropey = { version = "1.6.1", optional = true }
once_cell = "1.21.3"
syntect = { version = "5.2.0", optional = true, default-features = false, features = ["default-fancy"] }
[dev-dependencies]
tokio-test = "0.4.4"
@@ -37,6 +38,7 @@ cursor-style = ["crossterm"]
validation = ["regex"]
computed = []
textarea = ["dep:ropey","gui"]
syntect = ["dep:syntect", "gui", "textarea"]
# text modes (mutually exclusive; default to vim)
textmode-vim = []
@@ -50,7 +52,6 @@ all-nontextmodes = [
"computed",
"textarea"
]
ropey = ["dep:ropey"]
[[example]]
name = "suggestions"

View File

@@ -0,0 +1,360 @@
// examples/textarea_syntax.rs
//! Demonstrates syntax highlighting with the textarea widget
//!
//! This example REQUIRES the `syntect` feature to compile.
//!
//! Run with:
//! cargo run --example textarea_syntax --features "gui,cursor-style,textarea,syntect,textmode-normal"
#[cfg(not(feature = "syntect"))]
compile_error!(
"This example requires the 'syntect' feature. \
Run with: cargo run --example textarea_syntax --features \"gui,cursor-style,textarea,syntect,textmode-normal\""
);
use std::io;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
use canvas::{
canvas::CursorManager,
textarea::highlight::{TextAreaSyntax, TextAreaSyntaxState},
};
/// Syntax highlighting TextArea demo
struct SyntaxTextAreaDemo {
textarea: TextAreaSyntaxState,
has_unsaved_changes: bool,
debug_message: String,
current_language: String,
}
impl SyntaxTextAreaDemo {
fn new() -> Self {
let initial_text = r#"// 🎯 Syntax Highlighting Demo
fn main() {
println!("Hello, world!");
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers
.iter()
.map(|x| x * 2)
.collect();
for num in &doubled {
println!("Number: {}", num);
}
}
// Try pressing F5-F8 to switch languages!
// F1/F2: Switch overflow modes
// F3/F4: Adjust wrap indent"#;
let mut textarea = TextAreaSyntaxState::from_text(initial_text);
textarea.set_placeholder("Start typing code...");
// Set up syntax highlighting
let _ = textarea.set_syntax_theme("InspiredGitHub");
let _ = textarea.set_syntax_by_extension("rs");
Self {
textarea,
has_unsaved_changes: false,
debug_message: "🎯 Syntax highlighting enabled - Rust".to_string(),
current_language: "Rust".to_string(),
}
}
fn handle_textarea_input(&mut self, key: KeyEvent) {
self.textarea.input(key);
self.has_unsaved_changes = true;
}
fn switch_to_rust(&mut self) {
let _ = self.textarea.set_syntax_by_extension("rs");
self.current_language = "Rust".to_string();
self.debug_message = "🦀 Switched to Rust syntax".to_string();
let rust_code = r#"// Rust example
fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn main() {
for i in 0..10 {
println!("fib({}) = {}", i, fibonacci(i));
}
}"#;
self.textarea.set_text(rust_code);
}
fn switch_to_python(&mut self) {
let _ = self.textarea.set_syntax_by_extension("py");
self.current_language = "Python".to_string();
self.debug_message = "🐍 Switched to Python syntax".to_string();
let python_code = r#"# Python example
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
def main():
for i in range(10):
print(f"fib({i}) = {fibonacci(i)}")
if __name__ == "__main__":
main()"#;
self.textarea.set_text(python_code);
}
fn switch_to_javascript(&mut self) {
let _ = self.textarea.set_syntax_by_extension("js");
self.current_language = "JavaScript".to_string();
self.debug_message = "🟨 Switched to JavaScript syntax".to_string();
let js_code = r#"// JavaScript example
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
function main() {
for (let i = 0; i < 10; i++) {
console.log(`fib(${i}) = ${fibonacci(i)}`);
}
}
main();"#;
self.textarea.set_text(js_code);
}
fn switch_to_scheme(&mut self) {
let _ = self.textarea.set_syntax_by_name("Scheme");
self.current_language = "Scheme".to_string();
self.debug_message = "🎭 Switched to Scheme syntax".to_string();
let scheme_code = r#";; Scheme example
(define (fibonacci n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fibonacci (- n 1))
(fibonacci (- n 2))))))
(define (main)
(do ((i 0 (+ i 1)))
((= i 10))
(display (format "fib(~a) = ~a~n" i (fibonacci i)))))
(main)"#;
self.textarea.set_text(scheme_code);
}
fn get_cursor_info(&self) -> String {
format!(
"Line {}, Col {} | Lang: {}",
self.textarea.current_field() + 1,
self.textarea.cursor_position() + 1,
self.current_language
)
}
fn debug_message(&self) -> &str {
&self.debug_message
}
fn set_debug_message(&mut self, msg: String) {
self.debug_message = msg;
}
fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
}
fn handle_key_press(
key_event: KeyEvent,
editor: &mut SyntaxTextAreaDemo,
) -> anyhow::Result<bool> {
let KeyEvent {
code: key,
modifiers,
..
} = key_event;
// Quit
if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL))
|| (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL))
|| key == KeyCode::F(10)
{
return Ok(false);
}
match (key, modifiers) {
// Language switching
(KeyCode::F(5), _) => editor.switch_to_rust(),
(KeyCode::F(6), _) => editor.switch_to_python(),
(KeyCode::F(7), _) => editor.switch_to_javascript(),
(KeyCode::F(8), _) => editor.switch_to_scheme(),
// Overflow modes
(KeyCode::F(1), _) => {
editor.textarea.use_overflow_indicator('$');
editor.set_debug_message("Overflow: indicator '$' (wrap OFF)".to_string());
}
(KeyCode::F(2), _) => {
editor.textarea.use_wrap();
editor.set_debug_message("Overflow: wrap ON".to_string());
}
// Wrap indent
(KeyCode::F(3), _) => {
editor.textarea.set_wrap_indent_cols(4);
editor.set_debug_message("Wrap indent: 4 columns".to_string());
}
(KeyCode::F(4), _) => {
editor.textarea.set_wrap_indent_cols(0);
editor.set_debug_message("Wrap indent: 0 columns".to_string());
}
// Info
(KeyCode::Char('?'), _) => {
editor.set_debug_message(format!(
"{} | Syntax highlighting enabled",
editor.get_cursor_info()
));
}
// Default: pass to textarea
_ => editor.handle_textarea_input(key_event),
}
Ok(true)
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut editor: SyntaxTextAreaDemo) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &mut editor))?;
if let Event::Key(key) = event::read()? {
match handle_key_press(key, &mut editor) {
Ok(should_continue) => {
if !should_continue {
break;
}
}
Err(e) => {
editor.set_debug_message(format!("Error: {e}"));
}
}
}
}
Ok(())
}
fn ui(f: &mut Frame, editor: &mut SyntaxTextAreaDemo) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(8), Constraint::Length(8)])
.split(f.area());
render_textarea(f, chunks[0], editor);
render_status_and_help(f, chunks[1], editor);
}
fn render_textarea(f: &mut Frame, area: ratatui::layout::Rect, editor: &mut SyntaxTextAreaDemo) {
let block = Block::default()
.borders(Borders::ALL)
.title("🎨 Syntax Highlighted Code Editor");
let textarea_widget = TextAreaSyntax::default().block(block.clone());
f.render_stateful_widget(textarea_widget, area, &mut editor.textarea);
// Reuse cursor calculation from the wrapped textarea
let (cx, cy) = editor.textarea.cursor(area, Some(&block));
f.set_cursor_position((cx, cy));
}
fn render_status_and_help(f: &mut Frame, area: ratatui::layout::Rect, editor: &SyntaxTextAreaDemo) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Length(5)])
.split(area);
let status_text = if editor.has_unsaved_changes() {
format!(
"-- SYNTAX MODE (highlighting enabled) -- [Modified] {} | {}",
editor.debug_message(),
editor.get_cursor_info()
)
} else {
format!(
"-- SYNTAX MODE (highlighting enabled) -- {} | {}",
editor.debug_message(),
editor.get_cursor_info()
)
};
let status = Paragraph::new(Line::from(Span::raw(status_text)))
.block(Block::default().borders(Borders::ALL).title("🎨 Syntax Status"));
f.render_widget(status, chunks[0]);
let help_text = "🎨 SYNTAX HIGHLIGHTING DEMO\n\
F5=Rust, F6=Python, F7=JavaScript, F8=Scheme\n\
F1/F2=overflow modes, F3/F4=wrap indent\n\
?=info, Ctrl+Q=quit";
let help = Paragraph::new(help_text)
.block(Block::default().borders(Borders::ALL).title("🚀 Help"))
.style(Style::default().fg(Color::Cyan));
f.render_widget(help, chunks[1]);
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🎨 Canvas Textarea Syntax Highlighting Demo");
println!("✅ cursor-style feature: ENABLED");
println!("✅ textarea feature: ENABLED");
println!("✅ syntect feature: ENABLED");
println!("🎨 Syntax highlighting active");
println!();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let editor = SyntaxTextAreaDemo::new();
let res = run_app(&mut terminal, editor);
CursorManager::reset()?;
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
println!("🎨 Syntax highlighting demo complete!");
Ok(())
}

View File

@@ -0,0 +1,183 @@
// src/textarea/highlight/chunks.rs
use ratatui::text::{Line, Span};
use ratatui::style::Style;
use unicode_width::UnicodeWidthChar;
#[derive(Debug, Clone)]
pub struct StyledChunk {
pub text: String,
pub style: Style,
}
pub fn display_width_chunks(chunks: &[StyledChunk]) -> u16 {
chunks
.iter()
.map(|c| {
c.text
.chars()
.map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0) as u16)
.sum::<u16>()
})
.sum()
}
pub fn slice_chunks_by_display_cols(
chunks: &[StyledChunk],
start_cols: u16,
max_cols: u16,
) -> Vec<StyledChunk> {
if max_cols == 0 {
return Vec::new();
}
let mut skipped: u16 = 0;
let mut taken: u16 = 0;
let mut out: Vec<StyledChunk> = Vec::new();
for ch in chunks {
if taken >= max_cols {
break;
}
let mut acc = String::new();
for c in ch.text.chars() {
let w = UnicodeWidthChar::width(c).unwrap_or(0) as u16;
if skipped + w <= start_cols {
skipped += w;
continue;
}
if taken + w > max_cols {
break;
}
acc.push(c);
taken = taken.saturating_add(w);
if taken >= max_cols {
break;
}
}
if !acc.is_empty() {
out.push(StyledChunk {
text: acc,
style: ch.style,
});
}
}
out
}
pub fn clip_chunks_window_with_indicator_padded(
chunks: &[StyledChunk],
view_width: u16,
indicator: char,
start_cols: u16,
) -> Line<'static> {
if view_width == 0 {
return Line::from("");
}
let total = display_width_chunks(chunks);
let show_left = start_cols > 0;
let left_cols: u16 = if show_left { 1 } else { 0 };
let cap_with_right = view_width.saturating_sub(left_cols + 1);
let remaining = total.saturating_sub(start_cols);
let show_right = remaining > cap_with_right;
let max_visible = if show_right {
cap_with_right
} else {
view_width.saturating_sub(left_cols)
};
let visible = slice_chunks_by_display_cols(chunks, start_cols, max_visible);
let used_cols = left_cols + display_width_chunks(&visible);
let mut spans: Vec<Span> = Vec::new();
if show_left {
spans.push(Span::raw(indicator.to_string()));
}
for v in visible {
spans.push(Span::styled(v.text, v.style));
}
if show_right {
let right_pos = view_width.saturating_sub(1);
let filler = right_pos.saturating_sub(used_cols);
if filler > 0 {
spans.push(Span::raw(" ".repeat(filler as usize)));
}
spans.push(Span::raw(indicator.to_string()));
}
Line::from(spans)
}
pub fn wrap_chunks_indented(
chunks: &[StyledChunk],
width: u16,
indent: u16,
) -> Vec<Line<'static>> {
if width == 0 {
return vec![Line::from("")];
}
let indent = indent.min(width.saturating_sub(1));
let cont_cap = width.saturating_sub(indent);
let indent_str = " ".repeat(indent as usize);
let mut lines: Vec<Line> = Vec::new();
let mut current_spans: Vec<Span> = Vec::new();
let mut used: u16 = 0;
let mut first_line = true;
// Fixed: Restructure to avoid borrow checker issues
for chunk in chunks {
let mut buf = String::new();
let mut buf_style = chunk.style;
for ch in chunk.text.chars() {
let w = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
let cap = if first_line { width } else { cont_cap };
if used > 0 && used.saturating_add(w) >= cap {
if !buf.is_empty() {
current_spans.push(Span::styled(buf.clone(), buf_style));
buf.clear();
}
lines.push(Line::from(current_spans));
current_spans = Vec::new();
first_line = false;
used = 0;
// Add indent directly instead of using closure
if !first_line && indent > 0 {
current_spans.push(Span::raw(indent_str.clone()));
used = indent;
}
}
if !buf.is_empty() && buf_style != chunk.style {
current_spans.push(Span::styled(buf.clone(), buf_style));
buf.clear();
}
buf_style = chunk.style;
// Add indent if needed
if used == 0 && !first_line && indent > 0 {
current_spans.push(Span::raw(indent_str.clone()));
used = indent;
}
buf.push(ch);
used = used.saturating_add(w);
}
if !buf.is_empty() {
current_spans.push(Span::styled(buf, buf_style));
}
}
lines.push(Line::from(current_spans));
lines
}

View File

@@ -0,0 +1,269 @@
// src/textarea/highlight/engine.rs
#![allow(dead_code)]
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use ratatui::style::{Modifier, Style};
use syntect::{
highlighting::{
HighlightIterator, Highlighter, Style as SynStyle, Theme, ThemeSet,
},
parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
};
use crate::data_provider::DataProvider;
use super::chunks::StyledChunk;
#[derive(Debug)]
pub struct SyntectEngine {
ps: SyntaxSet,
ts: ThemeSet,
theme_name: String,
syntax_name: Option<String>,
// Cached parser state (after line i)
parse_after: Vec<ParseState>,
// Cached scope stack (after line i)
stack_after: Vec<ScopeStack>,
// Hash of line contents to detect edits
line_hashes: Vec<u64>,
}
impl SyntectEngine {
pub fn new() -> Self {
let ps = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
Self {
ps,
ts,
theme_name: "InspiredGitHub".to_string(),
syntax_name: None,
parse_after: Vec::new(),
stack_after: Vec::new(),
line_hashes: Vec::new(),
}
}
pub fn clear(&mut self) {
self.parse_after.clear();
self.stack_after.clear();
self.line_hashes.clear();
}
pub fn set_theme(&mut self, theme_name: &str) -> bool {
if self.ts.themes.contains_key(theme_name) {
self.theme_name = theme_name.to_string();
true
} else {
false
}
}
pub fn set_syntax_by_name(&mut self, name: &str) -> bool {
if self.ps.find_syntax_by_name(name).is_some() {
self.syntax_name = Some(name.to_string());
self.clear();
true
} else {
false
}
}
pub fn set_syntax_by_extension(&mut self, ext: &str) -> bool {
if let Some(s) = self.ps.find_syntax_by_extension(ext) {
self.syntax_name = Some(s.name.clone());
self.clear();
true
} else {
false
}
}
pub fn invalidate_from(&mut self, line_idx: usize) {
if line_idx < self.parse_after.len() {
self.parse_after.truncate(line_idx);
}
if line_idx < self.stack_after.len() {
self.stack_after.truncate(line_idx);
}
if line_idx < self.line_hashes.len() {
self.line_hashes.truncate(line_idx);
}
}
pub fn on_insert_line(&mut self, at: usize) {
self.invalidate_from(at);
}
pub fn on_delete_line(&mut self, at: usize) {
self.invalidate_from(at);
}
fn theme(&self) -> &Theme {
self.ts
.themes
.get(&self.theme_name)
.expect("theme exists")
}
fn syntax_ref(&self) -> &SyntaxReference {
if let Some(name) = &self.syntax_name {
if let Some(s) = self.ps.find_syntax_by_name(name) {
return s;
}
}
self.ps.find_syntax_plain_text()
}
fn map_syntect_style(s: SynStyle) -> Style {
let fg =
ratatui::style::Color::Rgb(s.foreground.r, s.foreground.g, s.foreground.b);
let mut st = Style::default().fg(fg);
use syntect::highlighting::FontStyle;
if s.font_style.contains(FontStyle::BOLD) {
st = st.add_modifier(Modifier::BOLD);
}
if s.font_style.contains(FontStyle::UNDERLINE) {
st = st.add_modifier(Modifier::UNDERLINED);
}
if s.font_style.contains(FontStyle::ITALIC) {
st = st.add_modifier(Modifier::ITALIC);
}
st
}
fn hash_line(s: &str) -> u64 {
let mut h = DefaultHasher::new();
s.hash(&mut h);
h.finish()
}
// Verify cached chain up to the nearest trusted predecessor of line_idx,
// using the provider to fetch the current lines.
fn verify_and_truncate_before(&mut self, line_idx: usize, provider: &dyn DataProvider) {
let mut k = std::cmp::min(line_idx, self.parse_after.len());
while k > 0 {
let j = k - 1;
let curr = Self::hash_line(provider.field_value(j));
if self.line_hashes.get(j) == Some(&curr) {
break;
}
self.invalidate_from(j);
k = j;
}
}
// Ensure we have parser + stack for lines [0..line_idx)
fn ensure_state_before(&mut self, line_idx: usize, provider: &dyn DataProvider) {
if line_idx == 0 || self.parse_after.len() >= line_idx {
return;
}
let syntax = self.syntax_ref();
let theme = self.theme();
let highlighter = Highlighter::new(theme);
let mut ps = if self.parse_after.is_empty() {
ParseState::new(syntax)
} else {
self.parse_after[self.parse_after.len() - 1].clone()
};
let mut stack = if self.stack_after.is_empty() {
ScopeStack::new()
} else {
self.stack_after[self.stack_after.len() - 1].clone()
};
let start = self.parse_after.len();
for i in start..line_idx {
let s = provider.field_value(i);
let ops = ps.parse_line(s);
// Advance stack by applying ops using HighlightIterator
let mut it = HighlightIterator::new(&highlighter, &ops[..], s, &mut stack);
while let Some((_style, _text)) = it.next() {
// Iterate to apply ops; we don't need the tokens here.
}
let h = Self::hash_line(s);
self.parse_after.push(ps.clone());
self.stack_after.push(stack.clone());
if i >= self.line_hashes.len() {
self.line_hashes.push(h);
} else {
self.line_hashes[i] = h;
}
}
}
// Highlight a single line using cached state; update caches for this line.
pub fn highlight_line_cached(
&mut self,
line_idx: usize,
line: &str,
provider: &dyn DataProvider,
) -> Vec<StyledChunk> {
// Auto-detect prior changes and truncate cache if needed
self.verify_and_truncate_before(line_idx, provider);
// Precompute states up to line_idx
self.ensure_state_before(line_idx, provider);
let syntax = self.syntax_ref();
let theme = self.theme();
let highlighter = Highlighter::new(theme);
let mut ps = if line_idx == 0 {
ParseState::new(syntax)
} else if self.parse_after.len() >= line_idx {
self.parse_after[line_idx - 1].clone()
} else {
ParseState::new(syntax)
};
let mut stack = if line_idx == 0 {
ScopeStack::new()
} else if self.stack_after.len() >= line_idx {
self.stack_after[line_idx - 1].clone()
} else {
ScopeStack::new()
};
let ops = ps.parse_line(line);
let mut iter = HighlightIterator::new(&highlighter, &ops[..], line, &mut stack);
let mut out: Vec<StyledChunk> = Vec::new();
while let Some((syn_style, slice)) = iter.next() {
if slice.is_empty() {
continue;
}
let text = slice.trim_end_matches('\n').to_string();
if text.is_empty() {
continue;
}
out.push(StyledChunk {
text,
style: Self::map_syntect_style(syn_style),
});
}
// Update caches for this line (state after this line)
let h = Self::hash_line(line);
if line_idx >= self.parse_after.len() {
self.parse_after.push(ps);
} else {
self.parse_after[line_idx] = ps;
}
if line_idx >= self.stack_after.len() {
self.stack_after.push(stack);
} else {
self.stack_after[line_idx] = stack;
}
if line_idx >= self.line_hashes.len() {
self.line_hashes.push(h);
} else {
self.line_hashes[line_idx] = h;
}
out
}
}

View File

@@ -0,0 +1,18 @@
// src/textarea/highlight/mod.rs
#[cfg(all(feature = "syntect", feature = "gui"))]
pub mod engine;
#[cfg(all(feature = "syntect", feature = "gui"))]
pub mod chunks;
#[cfg(all(feature = "syntect", feature = "gui"))]
pub mod state;
#[cfg(all(feature = "syntect", feature = "gui"))]
pub mod widget;
#[cfg(all(feature = "syntect", feature = "gui"))]
pub use engine::SyntectEngine;
#[cfg(all(feature = "syntect", feature = "gui"))]
pub use chunks::StyledChunk;
#[cfg(all(feature = "syntect", feature = "gui"))]
pub use state::TextAreaSyntaxState;
#[cfg(all(feature = "syntect", feature = "gui"))]
pub use widget::TextAreaSyntax;

View File

@@ -0,0 +1,53 @@
// src/textarea/highlight/state.rs
use std::ops::{Deref, DerefMut};
use super::engine::SyntectEngine;
use crate::textarea::state::TextAreaState;
// Remove Debug derive since TextAreaState doesn't implement Debug
pub struct TextAreaSyntaxState {
pub textarea: TextAreaState,
pub engine: SyntectEngine,
}
impl Default for TextAreaSyntaxState {
fn default() -> Self {
Self {
textarea: TextAreaState::default(),
engine: SyntectEngine::new(),
}
}
}
impl TextAreaSyntaxState {
pub fn from_text<S: Into<String>>(text: S) -> Self {
let mut s = Self::default();
s.textarea.set_text(text);
s
}
// Optional: convenience setters
pub fn set_syntax_theme(&mut self, theme: &str) -> bool {
self.engine.set_theme(theme)
}
pub fn set_syntax_by_name(&mut self, name: &str) -> bool {
self.engine.set_syntax_by_name(name)
}
pub fn set_syntax_by_extension(&mut self, ext: &str) -> bool {
self.engine.set_syntax_by_extension(ext)
}
}
impl Deref for TextAreaSyntaxState {
type Target = TextAreaState;
fn deref(&self) -> &Self::Target {
&self.textarea
}
}
impl DerefMut for TextAreaSyntaxState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.textarea
}
}

View File

@@ -0,0 +1,211 @@
// src/textarea/highlight/widget.rs
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Style,
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph, StatefulWidget, Widget},
};
use unicode_width::UnicodeWidthChar;
use super::chunks::{
clip_chunks_window_with_indicator_padded,
wrap_chunks_indented,
};
use super::state::TextAreaSyntaxState;
use crate::data_provider::DataProvider;
use crate::textarea::state::{
compute_h_scroll_with_padding, count_wrapped_rows_indented, TextOverflowMode,
};
#[derive(Debug, Clone)]
pub struct TextAreaSyntax<'a> {
pub block: Option<Block<'a>>,
pub style: Style,
pub border_type: BorderType,
}
impl<'a> Default for TextAreaSyntax<'a> {
fn default() -> Self {
Self {
block: Some(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
),
style: Style::default(),
border_type: BorderType::Rounded,
}
}
}
impl<'a> TextAreaSyntax<'a> {
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn border_type(mut self, ty: BorderType) -> Self {
self.border_type = ty;
if let Some(b) = &mut self.block {
*b = b.clone().border_type(ty);
}
self
}
}
fn display_width(s: &str) -> u16 {
s.chars()
.map(|c| UnicodeWidthChar::width(c).unwrap_or(0) as u16)
.sum()
}
fn display_cols_up_to(s: &str, char_count: usize) -> u16 {
let mut cols: u16 = 0;
for (i, ch) in s.chars().enumerate() {
if i >= char_count {
break;
}
cols = cols.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16);
}
cols
}
fn resolve_start_line_and_intra_indented(
state: &TextAreaSyntaxState,
inner: Rect,
) -> (usize, u16) {
let provider = state.textarea.editor.data_provider();
let total = provider.line_count();
if total == 0 {
return (0, 0);
}
let wrap = matches!(state.textarea.overflow_mode, TextOverflowMode::Wrap);
let width = inner.width;
let target_vis = state.textarea.scroll_y;
if !wrap {
let start = (target_vis as usize).min(total);
return (start, 0);
}
let indent = state.textarea.wrap_indent_cols;
let mut acc: u16 = 0;
for i in 0..total {
let s = provider.field_value(i);
let rows = count_wrapped_rows_indented(s, width, indent);
if acc.saturating_add(rows) > target_vis {
let intra = target_vis.saturating_sub(acc);
return (i, intra);
}
acc = acc.saturating_add(rows);
}
(total.saturating_sub(1), 0)
}
impl<'a> StatefulWidget for TextAreaSyntax<'a> {
type State = TextAreaSyntaxState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// Reuse existing scroll logic
state.textarea.ensure_visible(area, self.block.as_ref());
let inner = if let Some(b) = &self.block {
b.clone().render(area, buf);
b.inner(area)
} else {
area
};
let edited_now = state.textarea.take_edited_flag();
let wrap_mode = matches!(state.textarea.overflow_mode, TextOverflowMode::Wrap);
let provider = state.textarea.editor.data_provider();
let total = provider.line_count();
let (start, intra) = resolve_start_line_and_intra_indented(state, inner);
let mut display_lines: Vec<Line> = Vec::new();
if total == 0 || start >= total {
if let Some(ph) = &state.textarea.placeholder {
display_lines.push(Line::from(Span::raw(ph.clone())));
}
} else if wrap_mode {
let mut rows_left = inner.height;
let indent = state.textarea.wrap_indent_cols;
let mut i = start;
while i < total && rows_left > 0 {
let s = provider.field_value(i);
let chunks = state
.engine
.highlight_line_cached(i, s, provider);
let lines = wrap_chunks_indented(&chunks, inner.width, indent);
let skip = if i == start { intra as usize } else { 0 };
for l in lines.into_iter().skip(skip) {
display_lines.push(l);
rows_left = rows_left.saturating_sub(1);
if rows_left == 0 {
break;
}
}
i += 1;
}
} else {
let end = (start.saturating_add(inner.height as usize)).min(total);
for i in start..end {
let s = provider.field_value(i);
let chunks = state.engine.highlight_line_cached(i, s, provider);
let fits = display_width(s) <= inner.width;
let start_cols = if i == state.textarea.current_field() {
let col_idx = state.textarea.display_cursor_position();
let cursor_cols = display_cols_up_to(s, col_idx);
let (target_h, _left_cols) =
compute_h_scroll_with_padding(cursor_cols, inner.width);
if fits {
if edited_now {
target_h
} else {
0
}
} else {
target_h.max(state.textarea.h_scroll)
}
} else {
0
};
if let TextOverflowMode::Indicator { ch } = state.textarea.overflow_mode {
display_lines.push(clip_chunks_window_with_indicator_padded(
&chunks,
inner.width,
ch,
start_cols,
));
}
}
}
let p = Paragraph::new(display_lines)
.alignment(Alignment::Left)
.style(self.style);
p.render(inner, buf);
}
}

View File

@@ -1,8 +1,5 @@
// src/textarea/mod.rs
//! Text area convenience exports.
//!
//! Re-export the core textarea types and provider so consumers can use
//! `canvas::textarea::TextArea` / `TextAreaState` / `TextAreaProvider`.
pub mod provider;
pub mod state;
@@ -10,6 +7,9 @@ pub mod state;
#[cfg(feature = "gui")]
pub mod widget;
#[cfg(all(feature = "syntect", feature = "gui"))]
pub mod highlight;
pub use provider::TextAreaProvider;
pub use state::{TextAreaEditor, TextAreaState, TextOverflowMode};