syntec, but not compiling
This commit is contained in:
183
canvas/src/textarea/highlight/chunks.rs
Normal file
183
canvas/src/textarea/highlight/chunks.rs
Normal 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
|
||||
}
|
||||
269
canvas/src/textarea/highlight/engine.rs
Normal file
269
canvas/src/textarea/highlight/engine.rs
Normal 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
|
||||
}
|
||||
}
|
||||
18
canvas/src/textarea/highlight/mod.rs
Normal file
18
canvas/src/textarea/highlight/mod.rs
Normal 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;
|
||||
53
canvas/src/textarea/highlight/state.rs
Normal file
53
canvas/src/textarea/highlight/state.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
211
canvas/src/textarea/highlight/widget.rs
Normal file
211
canvas/src/textarea/highlight/widget.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user