Skip to main content

fresh/view/
soft_break.rs

1//! Soft break infrastructure
2//!
3//! Provides a marker-based system for injecting soft line breaks during rendering.
4//! Used for compose-mode word wrapping: plugins register break points at byte positions,
5//! and markers auto-adjust on buffer edits so breaks survive without async round-trips.
6//!
7//! ## Architecture
8//!
9//! Follows the same pattern as ConcealManager:
10//! 1. Plugins add soft breaks via `addSoftBreak(bufferId, namespace, position, indent)`
11//! 2. Break positions are stored with marker-based tracking (auto-adjust on edits)
12//! 3. During the token pipeline, breaks are injected into the token stream
13//!
14//! ## Integration Point
15//!
16//! Soft breaks are applied to the token stream in `split_rendering.rs` BEFORE
17//! conceal ranges and wrapping. This means:
18//! - Concealment operates on the already-broken lines
19//! - The wrapping transform sees pre-broken content
20
21use crate::model::marker::{MarkerId, MarkerList};
22use fresh_core::overlay::OverlayNamespace;
23
24/// A soft break point that injects a line break during rendering
25#[derive(Debug, Clone)]
26pub struct SoftBreakPoint {
27    /// Namespace for bulk operations (shared with overlay namespace system)
28    pub namespace: OverlayNamespace,
29
30    /// Marker at the break position (right affinity — shifts with inserted text)
31    pub marker_id: MarkerId,
32
33    /// Number of hanging indent spaces to insert after the break
34    pub indent: u16,
35}
36
37impl SoftBreakPoint {
38    /// Get the current byte position by resolving the marker
39    pub fn position(&self, marker_list: &MarkerList) -> usize {
40        marker_list.get_position(self.marker_id).unwrap_or(0)
41    }
42
43    /// Check if this break point falls within a byte range
44    pub fn in_range(&self, start: usize, end: usize, marker_list: &MarkerList) -> bool {
45        let pos = self.position(marker_list);
46        pos >= start && pos < end
47    }
48}
49
50/// Manages soft break points for a buffer
51#[derive(Debug, Clone)]
52pub struct SoftBreakManager {
53    breaks: Vec<SoftBreakPoint>,
54}
55
56impl SoftBreakManager {
57    /// Create a new empty soft break manager
58    pub fn new() -> Self {
59        Self { breaks: Vec::new() }
60    }
61
62    /// Add a soft break point
63    pub fn add(
64        &mut self,
65        marker_list: &mut MarkerList,
66        namespace: OverlayNamespace,
67        position: usize,
68        indent: u16,
69    ) {
70        let marker_id = marker_list.create(position, false); // right affinity
71
72        self.breaks.push(SoftBreakPoint {
73            namespace,
74            marker_id,
75            indent,
76        });
77    }
78
79    /// Remove all soft breaks in a namespace
80    pub fn clear_namespace(&mut self, namespace: &OverlayNamespace, marker_list: &mut MarkerList) {
81        let markers_to_delete: Vec<_> = self
82            .breaks
83            .iter()
84            .filter(|b| &b.namespace == namespace)
85            .map(|b| b.marker_id)
86            .collect();
87
88        self.breaks.retain(|b| &b.namespace != namespace);
89
90        for marker_id in markers_to_delete {
91            marker_list.delete(marker_id);
92        }
93    }
94
95    /// Remove all soft breaks that fall within a byte range and clean up their markers
96    pub fn remove_in_range(&mut self, start: usize, end: usize, marker_list: &mut MarkerList) {
97        let markers_to_delete: Vec<_> = self
98            .breaks
99            .iter()
100            .filter(|b| b.in_range(start, end, marker_list))
101            .map(|b| b.marker_id)
102            .collect();
103
104        self.breaks.retain(|b| !b.in_range(start, end, marker_list));
105
106        for marker_id in markers_to_delete {
107            marker_list.delete(marker_id);
108        }
109    }
110
111    /// Clear all soft breaks and their markers
112    pub fn clear(&mut self, marker_list: &mut MarkerList) {
113        for bp in &self.breaks {
114            marker_list.delete(bp.marker_id);
115        }
116        self.breaks.clear();
117    }
118
119    /// Query soft breaks that fall within a viewport range.
120    /// Returns sorted `(position, indent)` pairs for efficient token processing.
121    pub fn query_viewport(
122        &self,
123        start: usize,
124        end: usize,
125        marker_list: &MarkerList,
126    ) -> Vec<(usize, u16)> {
127        let mut results: Vec<(usize, u16)> = self
128            .breaks
129            .iter()
130            .filter_map(|b| {
131                let pos = b.position(marker_list);
132                if pos >= start && pos < end {
133                    Some((pos, b.indent))
134                } else {
135                    None
136                }
137            })
138            .collect();
139
140        // Sort by position for sequential processing
141        results.sort_by_key(|(pos, _)| *pos);
142
143        results
144    }
145
146    /// Returns true if there are no soft breaks
147    pub fn is_empty(&self) -> bool {
148        self.breaks.is_empty()
149    }
150}
151
152impl Default for SoftBreakManager {
153    fn default() -> Self {
154        Self::new()
155    }
156}