use super::style::{create_wrapped_virtual_lines, token_style_from_ratatui};
use crate::primitives::{ansi, display_width, visual_layout};
use crate::state::EditorState;
use crate::view::theme::Theme;
use crate::view::ui::view_pipeline::ViewLine;
use crate::view::virtual_text::VirtualTextPosition;
use fresh_core::api::{ViewTokenStyle, ViewTokenWire, ViewTokenWireKind};
use std::collections::{HashMap, HashSet};
struct BackUpPlan {
tail_start: usize,
tail_width: usize,
}
fn back_up_to_prior_space(
wrapped: &[ViewTokenWire],
line_indent: usize,
eff_width: usize,
) -> Option<BackUpPlan> {
use crate::primitives::visual_layout;
let mut space_idx: Option<usize> = None;
for (i, t) in wrapped.iter().enumerate().rev() {
match &t.kind {
ViewTokenWireKind::Break | ViewTokenWireKind::Newline => return None,
ViewTokenWireKind::Space => {
space_idx = Some(i);
break;
}
_ => continue,
}
}
let space_idx = space_idx?;
let tail_start = space_idx + 1;
if tail_start >= wrapped.len() {
return None;
}
if !wrapped[..space_idx]
.iter()
.any(|t| !matches!(t.kind, ViewTokenWireKind::Space))
{
return None;
}
let mut col = line_indent;
for t in &wrapped[tail_start..] {
match &t.kind {
ViewTokenWireKind::Text(s) => col += visual_layout::visual_width(s, col),
ViewTokenWireKind::Space => col += 1,
ViewTokenWireKind::BinaryByte(_) => col += 4,
_ => return None,
}
}
let tail_width = col.saturating_sub(line_indent);
if line_indent + tail_width > eff_width {
return None;
}
Some(BackUpPlan {
tail_start,
tail_width,
})
}
pub(crate) fn apply_wrapping_transform(
tokens: Vec<ViewTokenWire>,
content_width: usize,
gutter_width: usize,
hanging_indent: bool,
) -> Vec<ViewTokenWire> {
use visual_layout::visual_width;
use visual_layout::WRAP_MAX_LOOKBACK as MAX_LOOKBACK;
const MIN_CONTINUATION_CONTENT_WIDTH: usize = 10;
let available_width = content_width.saturating_sub(gutter_width);
if available_width < 2 {
return tokens;
}
let mut wrapped = Vec::new();
let mut current_line_width: usize = 0;
let mut line_indent: usize = 0;
let mut measuring_indent = hanging_indent;
let mut on_continuation = false;
#[inline]
fn effective_width(
available_width: usize,
_line_indent: usize,
_on_continuation: bool,
) -> usize {
available_width
}
fn emit_break_with_indent(
wrapped: &mut Vec<ViewTokenWire>,
current_line_width: &mut usize,
indent_string: &str,
) {
wrapped.push(ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Break,
style: None,
});
*current_line_width = 0;
if !indent_string.is_empty() {
wrapped.push(ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Text(indent_string.to_string()),
style: None,
});
*current_line_width = indent_string.len();
}
}
let mut cached_indent_string = String::new();
let mut cached_indent_len: usize = 0;
for token in tokens {
match &token.kind {
ViewTokenWireKind::Newline => {
wrapped.push(token);
current_line_width = 0;
line_indent = 0;
cached_indent_string.clear();
cached_indent_len = 0;
measuring_indent = hanging_indent;
on_continuation = false;
}
ViewTokenWireKind::Text(text) => {
if measuring_indent {
let mut ws_char_count = 0usize;
let mut ws_visual_width = 0usize;
for c in text.chars() {
if c == ' ' {
ws_visual_width += 1;
ws_char_count += 1;
} else if c == '\t' {
let tab_stop = 4;
let col = line_indent + ws_visual_width;
ws_visual_width += tab_stop - (col % tab_stop);
ws_char_count += 1;
} else {
break;
}
}
if ws_char_count == text.chars().count() {
line_indent += ws_visual_width;
} else {
line_indent += ws_visual_width;
measuring_indent = false;
}
if line_indent + MIN_CONTINUATION_CONTENT_WIDTH > available_width {
line_indent = 0;
}
if line_indent != cached_indent_len {
cached_indent_string = " ".repeat(line_indent);
cached_indent_len = line_indent;
}
}
let eff_width = effective_width(available_width, line_indent, on_continuation);
let text_visual_width = visual_width(text, current_line_width);
let fresh_line_capacity = eff_width.saturating_sub(line_indent);
let row_floor = eff_width.saturating_sub(MAX_LOOKBACK).max(eff_width / 2);
if current_line_width > 0
&& current_line_width + text_visual_width > eff_width
&& (text_visual_width <= fresh_line_capacity || current_line_width >= row_floor)
{
on_continuation = true;
emit_break_with_indent(
&mut wrapped,
&mut current_line_width,
&cached_indent_string,
);
}
let eff_width = effective_width(available_width, line_indent, on_continuation);
let text_visual_width = visual_width(text, current_line_width);
if current_line_width + text_visual_width > eff_width
&& !ansi::contains_ansi_codes(text)
{
use unicode_segmentation::UnicodeSegmentation;
let graphemes: Vec<(usize, &str)> = text.grapheme_indices(true).collect();
let mut grapheme_idx = 0;
let source_base = token.source_offset;
let word_bounds: Vec<usize> =
text.split_word_bound_indices().map(|(b, _)| b).collect();
let mut wb_lo: usize = 0;
while grapheme_idx < graphemes.len() {
let eff_width =
effective_width(available_width, line_indent, on_continuation);
let remaining_width = eff_width.saturating_sub(current_line_width);
if remaining_width == 0 {
on_continuation = true;
emit_break_with_indent(
&mut wrapped,
&mut current_line_width,
&cached_indent_string,
);
continue;
}
let mut chunk_visual_width = 0;
let mut chunk_grapheme_count = 0;
let mut col = current_line_width;
for &(_byte_offset, grapheme) in &graphemes[grapheme_idx..] {
let g_width = if grapheme == "\t" {
visual_layout::tab_expansion_width(col)
} else {
display_width::str_width(grapheme)
};
if chunk_visual_width + g_width > remaining_width
&& chunk_grapheme_count > 0
{
break;
}
chunk_visual_width += g_width;
chunk_grapheme_count += 1;
col += g_width;
}
if chunk_grapheme_count == 0 {
chunk_grapheme_count = 1;
let grapheme = graphemes[grapheme_idx].1;
chunk_visual_width = if grapheme == "\t" {
visual_layout::tab_expansion_width(current_line_width)
} else {
display_width::str_width(grapheme)
};
}
let mut force_break_after_push = false;
if chunk_grapheme_count > 1 {
let slice_start = graphemes[grapheme_idx].0;
let slice_end_hard =
if grapheme_idx + chunk_grapheme_count < graphemes.len() {
graphemes[grapheme_idx + chunk_grapheme_count].0
} else {
text.len()
};
let row_floor =
eff_width.saturating_sub(MAX_LOOKBACK).max(eff_width / 2);
let chunk_floor_from_cursor =
row_floor.saturating_sub(current_line_width);
let floor_byte = if chunk_floor_from_cursor < chunk_grapheme_count {
graphemes[grapheme_idx + chunk_floor_from_cursor].0
} else {
slice_end_hard
};
while wb_lo < word_bounds.len() && word_bounds[wb_lo] <= slice_start {
wb_lo += 1;
}
let mut wb_hi = wb_lo;
while wb_hi < word_bounds.len() && word_bounds[wb_hi] <= slice_end_hard
{
wb_hi += 1;
}
let mut best_target_byte = word_bounds[wb_lo..wb_hi]
.iter()
.rev()
.copied()
.find(|&b| b >= floor_byte);
let end_byte = text.len();
if best_target_byte.is_none()
&& end_byte > slice_start
&& end_byte >= floor_byte
&& end_byte <= slice_end_hard
{
best_target_byte = Some(end_byte);
} else if let Some(b) = best_target_byte {
if end_byte <= slice_end_hard
&& end_byte >= floor_byte
&& end_byte > b
{
best_target_byte = Some(end_byte);
}
}
if let Some(target_byte) = best_target_byte {
let new_count = graphemes[grapheme_idx..]
.iter()
.position(|(b, _)| *b == target_byte)
.unwrap_or(chunk_grapheme_count);
if new_count > 0 && new_count < chunk_grapheme_count {
chunk_grapheme_count = new_count;
let mut col = current_line_width;
chunk_visual_width = 0;
for &(_b, g) in
&graphemes[grapheme_idx..grapheme_idx + new_count]
{
let w = if g == "\t" {
visual_layout::tab_expansion_width(col)
} else {
display_width::str_width(g)
};
chunk_visual_width += w;
col += w;
}
force_break_after_push = true;
}
}
}
let chunk_start_byte = graphemes[grapheme_idx].0;
let chunk_end_byte =
if grapheme_idx + chunk_grapheme_count < graphemes.len() {
graphemes[grapheme_idx + chunk_grapheme_count].0
} else {
text.len()
};
let chunk = text[chunk_start_byte..chunk_end_byte].to_string();
let chunk_source = source_base.map(|b| b + chunk_start_byte);
wrapped.push(ViewTokenWire {
source_offset: chunk_source,
kind: ViewTokenWireKind::Text(chunk),
style: token.style.clone(),
});
current_line_width += chunk_visual_width;
grapheme_idx += chunk_grapheme_count;
let eff_width =
effective_width(available_width, line_indent, on_continuation);
if force_break_after_push || current_line_width >= eff_width {
on_continuation = true;
emit_break_with_indent(
&mut wrapped,
&mut current_line_width,
&cached_indent_string,
);
}
}
} else {
wrapped.push(token);
current_line_width += text_visual_width;
}
}
ViewTokenWireKind::Space => {
if measuring_indent {
line_indent += 1;
if line_indent + MIN_CONTINUATION_CONTENT_WIDTH > available_width {
line_indent = 0;
}
}
let eff_width = effective_width(available_width, line_indent, on_continuation);
if current_line_width + 1 > eff_width {
let back_up = back_up_to_prior_space(&wrapped, line_indent, eff_width);
if let Some(BackUpPlan {
tail_start,
tail_width,
}) = back_up
{
let tail: Vec<ViewTokenWire> = wrapped.drain(tail_start..).collect();
on_continuation = true;
emit_break_with_indent(
&mut wrapped,
&mut current_line_width,
&cached_indent_string,
);
wrapped.extend(tail);
current_line_width += tail_width;
} else {
on_continuation = true;
emit_break_with_indent(
&mut wrapped,
&mut current_line_width,
&cached_indent_string,
);
}
}
wrapped.push(token);
current_line_width += 1;
}
ViewTokenWireKind::Break => {
wrapped.push(token);
current_line_width = 0;
on_continuation = true;
if line_indent > 0 {
wrapped.push(ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Text(" ".repeat(line_indent)),
style: None,
});
current_line_width = line_indent;
}
}
ViewTokenWireKind::BinaryByte(_) => {
if measuring_indent {
measuring_indent = false;
}
let eff_width = effective_width(available_width, line_indent, on_continuation);
let byte_display_width = 4;
if current_line_width + byte_display_width > eff_width {
on_continuation = true;
emit_break_with_indent(
&mut wrapped,
&mut current_line_width,
&cached_indent_string,
);
}
wrapped.push(token);
current_line_width += byte_display_width;
}
}
}
wrapped
}
pub(crate) fn apply_soft_breaks(
tokens: Vec<ViewTokenWire>,
soft_breaks: &[(usize, u16)],
) -> Vec<ViewTokenWire> {
if soft_breaks.is_empty() {
return tokens;
}
let mut output = Vec::with_capacity(tokens.len() + soft_breaks.len() * 2);
let mut break_idx = 0;
for token in tokens {
let offset = match token.source_offset {
Some(o) => o,
None => {
output.push(token);
continue;
}
};
while break_idx < soft_breaks.len() && soft_breaks[break_idx].0 < offset {
break_idx += 1;
}
if break_idx < soft_breaks.len() && soft_breaks[break_idx].0 == offset {
let indent = soft_breaks[break_idx].1;
break_idx += 1;
match &token.kind {
ViewTokenWireKind::Space => {
output.push(ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Newline,
style: None,
});
for _ in 0..indent {
output.push(ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Space,
style: None,
});
}
}
_ => {
output.push(ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Newline,
style: None,
});
for _ in 0..indent {
output.push(ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Space,
style: None,
});
}
output.push(token);
}
}
} else {
output.push(token);
}
}
output
}
pub(crate) fn apply_conceal_ranges(
tokens: Vec<ViewTokenWire>,
conceal_ranges: &[(std::ops::Range<usize>, Option<&str>)],
) -> Vec<ViewTokenWire> {
if conceal_ranges.is_empty() {
return tokens;
}
let mut output = Vec::with_capacity(tokens.len());
let mut emitted_replacements: HashSet<usize> = HashSet::new();
let mut sorted: Vec<usize> = (0..conceal_ranges.len()).collect();
sorted.sort_by_key(|&i| conceal_ranges[i].0.start);
let mut conceal_cursor: usize = 0;
#[inline]
fn is_concealed(
conceal_ranges: &[(std::ops::Range<usize>, Option<&str>)],
sorted: &[usize],
cursor: &mut usize,
byte_offset: usize,
) -> Option<usize> {
while *cursor < sorted.len() && conceal_ranges[sorted[*cursor]].0.end <= byte_offset {
*cursor += 1;
}
let orig_idx = sorted.get(*cursor).copied()?;
let range = &conceal_ranges[orig_idx].0;
(range.start <= byte_offset && byte_offset < range.end).then_some(orig_idx)
}
for token in tokens {
let offset = match token.source_offset {
Some(o) => o,
None => {
output.push(token);
continue;
}
};
match &token.kind {
ViewTokenWireKind::Text(text) => {
let mut current_byte = offset;
let mut visible_start: Option<usize> = None;
let mut visible_chars = String::new();
for ch in text.chars() {
let ch_len = ch.len_utf8();
if let Some(cidx) =
is_concealed(conceal_ranges, &sorted, &mut conceal_cursor, current_byte)
{
if !visible_chars.is_empty() {
output.push(ViewTokenWire {
source_offset: visible_start,
kind: ViewTokenWireKind::Text(std::mem::take(&mut visible_chars)),
style: token.style.clone(),
});
visible_start = None;
}
if let Some(repl) = conceal_ranges[cidx].1 {
if !emitted_replacements.contains(&cidx) {
emitted_replacements.insert(cidx);
if !repl.is_empty() {
let mut chars = repl.chars();
if let Some(first_ch) = chars.next() {
output.push(ViewTokenWire {
source_offset: Some(conceal_ranges[cidx].0.start),
kind: ViewTokenWireKind::Text(first_ch.to_string()),
style: None,
});
let rest: String = chars.collect();
if !rest.is_empty() {
output.push(ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Text(rest),
style: None,
});
}
}
}
}
}
} else {
if visible_start.is_none() {
visible_start = Some(current_byte);
}
visible_chars.push(ch);
}
current_byte += ch_len;
}
if !visible_chars.is_empty() {
output.push(ViewTokenWire {
source_offset: visible_start,
kind: ViewTokenWireKind::Text(visible_chars),
style: token.style.clone(),
});
}
}
ViewTokenWireKind::Space | ViewTokenWireKind::Newline | ViewTokenWireKind::Break => {
if let Some(cidx) =
is_concealed(conceal_ranges, &sorted, &mut conceal_cursor, offset)
{
if let Some(repl) = conceal_ranges[cidx].1 {
if !emitted_replacements.contains(&cidx) {
emitted_replacements.insert(cidx);
if !repl.is_empty() {
let mut chars = repl.chars();
if let Some(first_ch) = chars.next() {
output.push(ViewTokenWire {
source_offset: Some(conceal_ranges[cidx].0.start),
kind: ViewTokenWireKind::Text(first_ch.to_string()),
style: None,
});
let rest: String = chars.collect();
if !rest.is_empty() {
output.push(ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Text(rest),
style: None,
});
}
}
}
}
}
} else {
output.push(token);
}
}
ViewTokenWireKind::BinaryByte(_) => {
if is_concealed(conceal_ranges, &sorted, &mut conceal_cursor, offset).is_some() {
} else {
output.push(token);
}
}
}
}
output
}
pub(super) fn inject_virtual_lines(
source_lines: Vec<ViewLine>,
state: &EditorState,
theme: &Theme,
wrap_width: Option<usize>,
) -> Vec<ViewLine> {
let viewport_start = source_lines
.first()
.and_then(|l| l.char_source_bytes.iter().find_map(|m| *m))
.unwrap_or(0);
let viewport_end = source_lines
.iter()
.rev()
.find_map(|l| l.char_source_bytes.iter().rev().find_map(|m| *m))
.map(|b| b + 1)
.unwrap_or(viewport_start);
let virtual_lines =
state
.virtual_texts
.query_lines_in_range(&state.marker_list, viewport_start, viewport_end);
if virtual_lines.is_empty() {
return source_lines;
}
let mut result = Vec::with_capacity(source_lines.len() + virtual_lines.len());
for source_line in source_lines {
let line_start_byte = source_line.char_source_bytes.iter().find_map(|m| *m);
let line_end_byte = source_line
.char_source_bytes
.iter()
.rev()
.find_map(|m| *m)
.map(|b| b + 1);
if let (Some(start), Some(end)) = (line_start_byte, line_end_byte) {
for (anchor_pos, vtext) in &virtual_lines {
if *anchor_pos >= start
&& *anchor_pos < end
&& vtext.position == VirtualTextPosition::LineAbove
{
let glyph = vtext.gutter_glyph.as_ref().map(|g| {
(
g.clone(),
vtext.gutter_color.unwrap_or(theme.line_number_fg),
)
});
result.extend(create_wrapped_virtual_lines(
&vtext.text,
vtext.resolved_style(theme),
wrap_width,
glyph,
&vtext.text_overlays,
));
}
}
}
result.push(source_line.clone());
if let (Some(start), Some(end)) = (line_start_byte, line_end_byte) {
for (anchor_pos, vtext) in &virtual_lines {
if *anchor_pos >= start
&& *anchor_pos < end
&& vtext.position == VirtualTextPosition::LineBelow
{
let glyph = vtext.gutter_glyph.as_ref().map(|g| {
(
g.clone(),
vtext.gutter_color.unwrap_or(theme.line_number_fg),
)
});
result.extend(create_wrapped_virtual_lines(
&vtext.text,
vtext.resolved_style(theme),
wrap_width,
glyph,
&vtext.text_overlays,
));
}
}
}
}
result
}
struct InlineHintCell {
text: String,
style: Option<ViewTokenStyle>,
}
pub fn splice_inline_virtual_text(
tokens: Vec<ViewTokenWire>,
state: &EditorState,
theme: Option<&Theme>,
start: usize,
end: usize,
) -> Vec<ViewTokenWire> {
let inline = state
.virtual_texts
.query_inline_in_range(&state.marker_list, start, end);
if inline.is_empty() {
return tokens;
}
let mut before: HashMap<usize, Vec<(String, Option<ViewTokenStyle>)>> = HashMap::new();
let mut after: HashMap<usize, Vec<InlineHintCell>> = HashMap::new();
for (pos, vtext) in inline {
let style = theme.map(|t| token_style_from_ratatui(vtext.resolved_style(t)));
match vtext.position {
VirtualTextPosition::BeforeChar => {
before
.entry(pos)
.or_default()
.push((vtext.text.clone(), style));
}
VirtualTextPosition::AfterChar => {
after.entry(pos).or_default().push(InlineHintCell {
text: format!(" {}", vtext.text),
style,
});
}
_ => {}
}
}
let virt = |text: String, style: Option<ViewTokenStyle>| ViewTokenWire {
source_offset: None,
kind: ViewTokenWireKind::Text(text),
style,
};
let mut out: Vec<ViewTokenWire> = Vec::with_capacity(tokens.len());
for token in tokens {
let src = token.source_offset;
match (&token.kind, src) {
(ViewTokenWireKind::Text(s), Some(token_start)) => {
let mut seg = String::new();
let mut seg_start = token_start;
let mut byte_idx = 0usize;
for ch in s.chars() {
let anchor = token_start + byte_idx;
if let Some(hints) = before.get(&anchor) {
if !seg.is_empty() {
out.push(ViewTokenWire {
source_offset: Some(seg_start),
kind: ViewTokenWireKind::Text(std::mem::take(&mut seg)),
style: token.style.clone(),
});
}
seg_start = anchor;
for (text, style) in hints {
out.push(virt(format!("{text} "), style.clone()));
}
}
seg.push(ch);
byte_idx += ch.len_utf8();
if let Some(hints) = after.get(&anchor) {
out.push(ViewTokenWire {
source_offset: Some(seg_start),
kind: ViewTokenWireKind::Text(std::mem::take(&mut seg)),
style: token.style.clone(),
});
seg_start = token_start + byte_idx;
for hint in hints {
out.push(virt(hint.text.clone(), hint.style.clone()));
}
}
}
if !seg.is_empty() {
out.push(ViewTokenWire {
source_offset: Some(seg_start),
kind: ViewTokenWireKind::Text(seg),
style: token.style.clone(),
});
}
}
(kind, Some(anchor)) => {
let anchor_is_newline = matches!(kind, ViewTokenWireKind::Newline);
if let Some(hints) = before.get(&anchor) {
for (text, style) in hints {
let padded = if anchor_is_newline {
format!(" {text} ")
} else {
format!("{text} ")
};
out.push(virt(padded, style.clone()));
}
}
let after_hints = after.get(&anchor);
out.push(token);
if let Some(hints) = after_hints {
for hint in hints {
out.push(virt(hint.text.clone(), hint.style.clone()));
}
}
}
_ => out.push(token),
}
}
out
}