Skip to main content

fresh/app/
virtual_buffers.rs

1//! Stdin streaming and virtual buffer creation on `Editor`.
2//!
3//! - open_stdin_buffer / poll_stdin_streaming / complete_stdin_streaming /
4//!   is_stdin_streaming: drive the StdinStream subsystem (extracted in
5//!   phase 2e), translating its outcomes into buffer extensions and
6//!   status messages.
7//! - create_virtual_buffer / set_virtual_buffer_content: helpers for
8//!   creating buffers backed by virtual content (LSP help text, plugin
9//!   panels, search results, etc.).
10
11use std::path::Path;
12use std::sync::Arc;
13
14use anyhow::Result as AnyhowResult;
15use rust_i18n::t;
16
17use crate::model::event::BufferId;
18use crate::state::EditorState;
19use crate::view::split::SplitViewState;
20
21use super::Editor;
22
23impl Editor {
24    /// The temp file path is preserved internally for lazy loading to work.
25    ///
26    /// # Arguments
27    /// * `temp_path` - Path to temp file where stdin content is being written
28    /// * `thread_handle` - Optional handle to background thread streaming stdin to temp file
29    pub fn open_stdin_buffer(
30        &mut self,
31        temp_path: &Path,
32        thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
33    ) -> AnyhowResult<BufferId> {
34        // Save current position before switching to new buffer
35        self.position_history.commit_pending_movement();
36
37        // Explicitly record current position before switching
38        let cursors = self.active_cursors();
39        let position = cursors.primary().position;
40        let anchor = cursors.primary().anchor;
41        self.position_history
42            .record_movement(self.active_buffer(), position, anchor);
43        self.position_history.commit_pending_movement();
44
45        // If the current buffer is empty and unmodified, replace it instead of creating a new one
46        // Note: Don't replace composite buffers (they appear empty but are special views)
47        let replace_current = {
48            let current_state = self.buffers.get(&self.active_buffer()).unwrap();
49            !current_state.is_composite_buffer
50                && current_state.buffer.is_empty()
51                && !current_state.buffer.is_modified()
52                && current_state.buffer.file_path().is_none()
53        };
54
55        let buffer_id = if replace_current {
56            // Reuse the current empty buffer
57            self.active_buffer()
58        } else {
59            // Create new buffer ID
60            let id = BufferId(self.next_buffer_id);
61            self.next_buffer_id += 1;
62            id
63        };
64
65        // Get file size for status message before loading
66        let file_size = self.filesystem.metadata(temp_path)?.size as usize;
67
68        // Load from temp file using EditorState::from_file_with_languages
69        // This enables lazy chunk loading for large inputs (>100MB by default)
70        let mut state = EditorState::from_file_with_languages(
71            temp_path,
72            self.terminal_width,
73            self.terminal_height,
74            self.config.editor.large_file_threshold_bytes as usize,
75            &self.grammar_registry,
76            &self.config.languages,
77            Arc::clone(&self.filesystem),
78        )?;
79
80        // Clear the file path so the buffer is "unnamed" for save purposes
81        // The Unloaded chunks still reference the temp file for lazy loading
82        state.buffer.clear_file_path();
83        // Clear modified flag - content is "fresh" from stdin (vim behavior)
84        state.buffer.clear_modified();
85
86        // Set tab size, auto_close, and auto_surround from config
87        state.buffer_settings.tab_size = self.config.editor.tab_size;
88        state.buffer_settings.auto_close = self.config.editor.auto_close;
89        state.buffer_settings.auto_surround = self.config.editor.auto_surround;
90
91        // Apply line_numbers default from config
92        state
93            .margins
94            .configure_for_line_numbers(self.config.editor.line_numbers);
95
96        self.buffers.insert(buffer_id, state);
97        self.event_logs
98            .insert(buffer_id, crate::model::event::EventLog::new());
99
100        // Create metadata for this buffer (no file path)
101        let metadata =
102            super::types::BufferMetadata::new_unnamed(t!("stdin.display_name").to_string());
103        self.buffer_metadata.insert(buffer_id, metadata);
104
105        // Add buffer to the active split's tabs
106        let active_split = self.split_manager.active_split();
107        let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
108        let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
109        if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
110            view_state.add_buffer(buffer_id);
111            let buf_state = view_state.ensure_buffer_state(buffer_id);
112            buf_state.apply_config_defaults(
113                self.config.editor.line_numbers,
114                self.config.editor.highlight_current_line,
115                line_wrap,
116                self.config.editor.wrap_indent,
117                wrap_column,
118                self.config.editor.rulers.clone(),
119            );
120        }
121
122        self.set_active_buffer(buffer_id);
123
124        // Set up stdin streaming state for polling.
125        // If no thread handle, the subsystem starts already-complete — used
126        // by tests and the "stdin was fully drained before we started" case.
127        self.stdin_stream
128            .start(temp_path.to_path_buf(), buffer_id, file_size, thread_handle);
129
130        // Status will be updated by poll_stdin_streaming
131        self.status_message = Some(t!("stdin.streaming").to_string());
132
133        Ok(buffer_id)
134    }
135
136    /// Poll stdin streaming state and extend buffer if file grew.
137    /// Returns true if the status changed (needs render).
138    pub fn poll_stdin_streaming(&mut self) -> bool {
139        use super::stdin_stream::ThreadOutcome;
140
141        if !self.stdin_stream.is_active() {
142            return false;
143        }
144
145        let Some(buffer_id) = self.stdin_stream.buffer_id() else {
146            return false;
147        };
148        let temp_path = self.stdin_stream.temp_path().unwrap().to_path_buf();
149        let last_known = self.stdin_stream.last_known_size();
150
151        let mut changed = false;
152
153        // Check current file size
154        let current_size = self
155            .filesystem
156            .metadata(&temp_path)
157            .map(|m| m.size as usize)
158            .unwrap_or(last_known);
159
160        // If file grew, extend the buffer
161        if self.stdin_stream.record_growth(current_size) {
162            if let Some(editor_state) = self.buffers.get_mut(&buffer_id) {
163                editor_state
164                    .buffer
165                    .extend_streaming(&temp_path, current_size);
166            }
167            self.status_message =
168                Some(t!("stdin.streaming_bytes", bytes = current_size).to_string());
169            changed = true;
170        }
171
172        // Drain a just-finished thread and surface its outcome to the user.
173        if let Some(outcome) = self.stdin_stream.take_finished_thread_outcome() {
174            match outcome {
175                ThreadOutcome::Success => {
176                    tracing::info!("Stdin streaming completed successfully");
177                }
178                ThreadOutcome::Error(msg) => {
179                    tracing::warn!("Stdin streaming error: {}", msg);
180                    self.status_message = Some(t!("stdin.read_error", error = msg).to_string());
181                }
182                ThreadOutcome::Panic => {
183                    tracing::warn!("Stdin streaming thread panicked");
184                    self.status_message = Some(t!("stdin.read_error_panic").to_string());
185                }
186            }
187            self.complete_stdin_streaming();
188            changed = true;
189        }
190
191        changed
192    }
193
194    /// Mark stdin streaming as complete.
195    /// Called when the background thread finishes.
196    pub fn complete_stdin_streaming(&mut self) {
197        let Some(buffer_id) = self.stdin_stream.buffer_id() else {
198            return;
199        };
200        let Some(temp_path) = self.stdin_stream.temp_path().map(Path::to_path_buf) else {
201            return;
202        };
203
204        self.stdin_stream.mark_complete();
205
206        // Final poll to get any remaining data
207        let final_size = self
208            .filesystem
209            .metadata(&temp_path)
210            .map(|m| m.size as usize)
211            .unwrap_or(self.stdin_stream.last_known_size());
212
213        if self.stdin_stream.record_growth(final_size) {
214            if let Some(editor_state) = self.buffers.get_mut(&buffer_id) {
215                editor_state.buffer.extend_streaming(&temp_path, final_size);
216            }
217        }
218
219        self.status_message = Some(
220            t!(
221                "stdin.read_complete",
222                bytes = self.stdin_stream.last_known_size()
223            )
224            .to_string(),
225        );
226    }
227
228    /// Check if stdin streaming is active (not complete).
229    pub fn is_stdin_streaming(&self) -> bool {
230        self.stdin_stream.is_active()
231    }
232
233    /// Create a new virtual buffer (not backed by a file)
234    ///
235    /// # Arguments
236    /// * `name` - Display name (e.g., "*Diagnostics*")
237    /// * `mode` - Buffer mode for keybindings (e.g., "diagnostics-list")
238    /// * `read_only` - Whether the buffer should be read-only
239    ///
240    /// # Returns
241    /// The BufferId of the created virtual buffer
242    pub fn create_virtual_buffer(
243        &mut self,
244        name: String,
245        mode: String,
246        read_only: bool,
247    ) -> BufferId {
248        let buffer_id = BufferId(self.next_buffer_id);
249        self.next_buffer_id += 1;
250
251        let mut state = EditorState::new(
252            self.terminal_width,
253            self.terminal_height,
254            self.config.editor.large_file_threshold_bytes as usize,
255            Arc::clone(&self.filesystem),
256        );
257        // Note: line_wrap_enabled is set on SplitViewState.viewport when the split is created
258
259        // Set syntax highlighting based on buffer name (e.g., "*OURS*.c" will get C highlighting)
260        state.set_language_from_name(&name, &self.grammar_registry);
261
262        // Apply line_numbers default from config
263        state
264            .margins
265            .configure_for_line_numbers(self.config.editor.line_numbers);
266
267        self.buffers.insert(buffer_id, state);
268        self.event_logs
269            .insert(buffer_id, crate::model::event::EventLog::new());
270
271        // Set virtual buffer metadata
272        let metadata = super::types::BufferMetadata::virtual_buffer(name, mode, read_only);
273        self.buffer_metadata.insert(buffer_id, metadata);
274
275        // Add buffer to the active split's open_buffers (tabs)
276        let active_split = self.split_manager.active_split();
277        let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
278        let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
279        if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
280            view_state.add_buffer(buffer_id);
281            let buf_state = view_state.ensure_buffer_state(buffer_id);
282            buf_state.apply_config_defaults(
283                self.config.editor.line_numbers,
284                self.config.editor.highlight_current_line,
285                line_wrap,
286                self.config.editor.wrap_indent,
287                wrap_column,
288                self.config.editor.rulers.clone(),
289            );
290        } else {
291            // Create view state if it doesn't exist
292            let mut view_state =
293                SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buffer_id);
294            view_state.apply_config_defaults(
295                self.config.editor.line_numbers,
296                self.config.editor.highlight_current_line,
297                line_wrap,
298                self.config.editor.wrap_indent,
299                wrap_column,
300                self.config.editor.rulers.clone(),
301            );
302            self.split_view_states.insert(active_split, view_state);
303        }
304
305        buffer_id
306    }
307
308    /// Set the content of a virtual buffer with text properties
309    ///
310    /// # Arguments
311    /// * `buffer_id` - The virtual buffer to update
312    /// * `entries` - Text entries with embedded properties
313    pub fn set_virtual_buffer_content(
314        &mut self,
315        buffer_id: BufferId,
316        entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
317    ) -> Result<(), String> {
318        let state = self
319            .buffers
320            .get_mut(&buffer_id)
321            .ok_or_else(|| "Buffer not found".to_string())?;
322
323        // Build text and properties from entries
324        let (text, properties, collected_overlays) =
325            crate::primitives::text_property::TextPropertyManager::from_entries(entries);
326
327        // Replace buffer content
328        // Note: we use buffer.delete_bytes/insert directly (not state.delete_range/insert_text_at)
329        // which bypasses marker_list adjustment. Clear ALL overlays first so no stale markers
330        // remain pointing at invalid positions in the new content.
331        state.overlays.clear(&mut state.marker_list);
332
333        let current_len = state.buffer.len();
334        if current_len > 0 {
335            state.buffer.delete_bytes(0, current_len);
336        }
337        state.buffer.insert(0, &text);
338
339        // Clear modified flag since this is virtual buffer content setting, not user edits
340        state.buffer.clear_modified();
341
342        // Set text properties
343        state.text_properties = properties;
344
345        // Create inline overlays for the new content. Build the full vec
346        // first and bulk-add it so the OverlayManager sorts exactly once;
347        // a per-overlay `add` re-sorts every time and is O(n² log n) for
348        // N entries (a big git-show diff can be ~500k overlays).
349        {
350            use crate::view::overlay::{Overlay, OverlayFace};
351            use fresh_core::overlay::OverlayNamespace;
352
353            let inline_ns = OverlayNamespace::from_string("_inline".to_string());
354            let mut new_overlays = Vec::with_capacity(collected_overlays.len());
355
356            for co in collected_overlays {
357                let face = OverlayFace::from_options(&co.options);
358                let mut overlay = Overlay::with_namespace(
359                    &mut state.marker_list,
360                    co.range,
361                    face,
362                    inline_ns.clone(),
363                );
364                overlay.extend_to_line_end = co.options.extend_to_line_end;
365                if let Some(url) = co.options.url {
366                    overlay.url = Some(url);
367                }
368                new_overlays.push(overlay);
369            }
370            state.overlays.extend(new_overlays);
371        }
372
373        // Each split keeps its own cursor; just clamp anything that fell
374        // past the new buffer end and snap to a char boundary. Don't read
375        // one split's cursor and write it into the others.
376        let new_len = state.buffer.len();
377        // `state` is no longer used past this point — re-borrow `self.buffers`
378        // immutably for the snap and `self.split_view_states` mutably for the
379        // write. These are disjoint fields of `self`.
380        let buffer = &self
381            .buffers
382            .get(&buffer_id)
383            .expect("buffer still present")
384            .buffer;
385        for view_state in self.split_view_states.values_mut() {
386            let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) else {
387                continue;
388            };
389            buf_state.cursors.map(|cursor| {
390                let pos = cursor.position.min(new_len);
391                cursor.position = buffer.snap_to_char_boundary(pos);
392                if let Some(anchor) = cursor.anchor {
393                    let clamped = anchor.min(new_len);
394                    cursor.anchor = Some(buffer.snap_to_char_boundary(clamped));
395                }
396            });
397        }
398        Ok(())
399    }
400}