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.active_window_mut()
36            .position_history
37            .commit_pending_movement();
38
39        // Explicitly record current position before switching
40        let cursors = self.active_cursors();
41        let position = cursors.primary().position;
42        let anchor = cursors.primary().anchor;
43        let active_buffer_id = self.active_buffer();
44        let ph = &mut self.active_window_mut().position_history;
45        ph.record_movement(active_buffer_id, position, anchor);
46        ph.commit_pending_movement();
47
48        // If the current buffer is empty and unmodified, replace it instead of creating a new one
49        // Note: Don't replace composite buffers (they appear empty but are special views)
50        let replace_current = {
51            let current_state = self
52                .windows
53                .get(&self.active_window)
54                .map(|w| &w.buffers)
55                .expect("active window present")
56                .get(&self.active_buffer())
57                .unwrap();
58            !current_state.is_composite_buffer
59                && current_state.buffer.is_empty()
60                && !current_state.buffer.is_modified()
61                && current_state.buffer.file_path().is_none()
62        };
63
64        let buffer_id = if replace_current {
65            // Reuse the current empty buffer
66            self.active_buffer()
67        } else {
68            // Create new buffer ID
69            let id = self.alloc_buffer_id();
70            id
71        };
72
73        // Get file size for status message before loading
74        let file_size = self.authority().filesystem.metadata(temp_path)?.size as usize;
75
76        // Load from temp file using EditorState::from_file_with_languages
77        // This enables lazy chunk loading for large inputs (>100MB by default)
78        let mut state = EditorState::from_file_with_languages(
79            temp_path,
80            self.terminal_width,
81            self.terminal_height,
82            self.config.editor.large_file_threshold_bytes as usize,
83            &self.grammar_registry,
84            &self.config.languages,
85            Arc::clone(&self.authority().filesystem),
86        )?;
87
88        // Clear the file path so the buffer is "unnamed" for save purposes
89        // The Unloaded chunks still reference the temp file for lazy loading
90        state.buffer.clear_file_path();
91        // Clear modified flag - content is "fresh" from stdin (vim behavior)
92        state.buffer.clear_modified();
93
94        // Set tab size, auto_close, and auto_surround from config
95        state.buffer_settings.tab_size = self.config.editor.tab_size;
96        state.buffer_settings.auto_close = self.config.editor.auto_close;
97        state.buffer_settings.auto_surround = self.config.editor.auto_surround;
98
99        // Apply line_numbers default from config
100        state
101            .margins
102            .configure_for_line_numbers(self.config.editor.line_numbers);
103
104        self.windows
105            .get_mut(&self.active_window)
106            .map(|w| &mut w.buffers)
107            .expect("active window present")
108            .insert(buffer_id, state);
109        self.active_window_mut()
110            .event_logs
111            .insert(buffer_id, crate::model::event::EventLog::new());
112
113        // Create metadata for this buffer (no file path)
114        let metadata =
115            super::types::BufferMetadata::new_unnamed(t!("stdin.display_name").to_string());
116        self.active_window_mut()
117            .buffer_metadata
118            .insert(buffer_id, metadata);
119
120        // Add buffer to the active split's tabs
121        let active_split = self
122            .windows
123            .get(&self.active_window)
124            .and_then(|w| w.buffers.splits())
125            .map(|(mgr, _)| mgr)
126            .expect("active window must have a populated split layout")
127            .active_split();
128        let line_wrap = self.active_window().resolve_line_wrap_for_buffer(buffer_id);
129        let wrap_column = self
130            .active_window()
131            .resolve_wrap_column_for_buffer(buffer_id);
132        if let Some(view_state) = self
133            .windows
134            .get_mut(&self.active_window)
135            .and_then(|w| w.split_view_states_mut())
136            .expect("active window must have a populated split layout")
137            .get_mut(&active_split)
138        {
139            view_state.add_buffer(buffer_id);
140            let buf_state = view_state.ensure_buffer_state(buffer_id);
141            buf_state.apply_config_defaults(
142                self.config.editor.line_numbers,
143                self.config.editor.highlight_current_line,
144                line_wrap,
145                self.config.editor.wrap_indent,
146                wrap_column,
147                self.config.editor.rulers.clone(),
148                self.config.editor.scroll_offset,
149            );
150        }
151
152        self.set_active_buffer(buffer_id);
153
154        // Set up stdin streaming state for polling.
155        // If no thread handle, the subsystem starts already-complete — used
156        // by tests and the "stdin was fully drained before we started" case.
157        self.stdin_stream
158            .start(temp_path.to_path_buf(), buffer_id, file_size, thread_handle);
159
160        // Status will be updated by poll_stdin_streaming
161        self.active_window_mut().status_message = Some(t!("stdin.streaming").to_string());
162
163        Ok(buffer_id)
164    }
165
166    /// Poll stdin streaming state and extend buffer if file grew.
167    /// Returns true if the status changed (needs render).
168    pub fn poll_stdin_streaming(&mut self) -> bool {
169        use super::stdin_stream::ThreadOutcome;
170
171        if !self.stdin_stream.is_active() {
172            return false;
173        }
174
175        let Some(buffer_id) = self.stdin_stream.buffer_id() else {
176            return false;
177        };
178        let temp_path = self.stdin_stream.temp_path().unwrap().to_path_buf();
179        let last_known = self.stdin_stream.last_known_size();
180
181        let mut changed = false;
182
183        // Check current file size
184        let current_size = self
185            .authority()
186            .filesystem
187            .metadata(&temp_path)
188            .map(|m| m.size as usize)
189            .unwrap_or(last_known);
190
191        // If file grew, extend the buffer
192        if self.stdin_stream.record_growth(current_size) {
193            if let Some(editor_state) = self
194                .windows
195                .get_mut(&self.active_window)
196                .map(|w| &mut w.buffers)
197                .expect("active window present")
198                .get_mut(&buffer_id)
199            {
200                editor_state
201                    .buffer
202                    .extend_streaming(&temp_path, current_size);
203            }
204            self.active_window_mut().status_message =
205                Some(t!("stdin.streaming_bytes", bytes = current_size).to_string());
206            changed = true;
207        }
208
209        // Drain a just-finished thread and surface its outcome to the user.
210        if let Some(outcome) = self.stdin_stream.take_finished_thread_outcome() {
211            match outcome {
212                ThreadOutcome::Success => {
213                    tracing::info!("Stdin streaming completed successfully");
214                }
215                ThreadOutcome::Error(msg) => {
216                    tracing::warn!("Stdin streaming error: {}", msg);
217                    self.active_window_mut().status_message =
218                        Some(t!("stdin.read_error", error = msg).to_string());
219                }
220                ThreadOutcome::Panic => {
221                    tracing::warn!("Stdin streaming thread panicked");
222                    self.active_window_mut().status_message =
223                        Some(t!("stdin.read_error_panic").to_string());
224                }
225            }
226            self.complete_stdin_streaming();
227            changed = true;
228        }
229
230        changed
231    }
232
233    /// Mark stdin streaming as complete.
234    /// Called when the background thread finishes.
235    pub fn complete_stdin_streaming(&mut self) {
236        let Some(buffer_id) = self.stdin_stream.buffer_id() else {
237            return;
238        };
239        let Some(temp_path) = self.stdin_stream.temp_path().map(Path::to_path_buf) else {
240            return;
241        };
242
243        self.stdin_stream.mark_complete();
244
245        // Final poll to get any remaining data
246        let final_size = self
247            .authority()
248            .filesystem
249            .metadata(&temp_path)
250            .map(|m| m.size as usize)
251            .unwrap_or(self.stdin_stream.last_known_size());
252
253        if self.stdin_stream.record_growth(final_size) {
254            if let Some(editor_state) = self
255                .windows
256                .get_mut(&self.active_window)
257                .map(|w| &mut w.buffers)
258                .expect("active window present")
259                .get_mut(&buffer_id)
260            {
261                editor_state.buffer.extend_streaming(&temp_path, final_size);
262            }
263        }
264
265        self.active_window_mut().status_message = Some(
266            t!(
267                "stdin.read_complete",
268                bytes = self.stdin_stream.last_known_size()
269            )
270            .to_string(),
271        );
272    }
273
274    /// Check if stdin streaming is active (not complete).
275    pub fn is_stdin_streaming(&self) -> bool {
276        self.stdin_stream.is_active()
277    }
278
279    /// Create a new virtual buffer (not backed by a file)
280    ///
281    /// # Arguments
282    /// * `name` - Display name (e.g., "*Diagnostics*")
283    /// * `mode` - Buffer mode for keybindings (e.g., "diagnostics-list")
284    /// * `read_only` - Whether the buffer should be read-only
285    ///
286    /// # Returns
287    /// The BufferId of the created virtual buffer
288    ///
289    /// Like [`Self::create_virtual_buffer`] but does **not** add the
290    /// new buffer to any split's tab list. Use this when the caller
291    /// is going to seed a freshly-created split (e.g. the Utility
292    /// Dock leaf) with the new buffer directly — without it, the
293    /// buffer would briefly appear as a phantom tab in whatever the
294    /// previously-active split was, requiring a separate cleanup
295    /// pass to remove it.
296    // `create_virtual_buffer_detached` and `create_virtual_buffer` live
297    // on `impl Window` — call them via
298    // `self.active_window_mut().create_virtual_buffer*(...)`.
299
300    /// Set the content of a virtual buffer with text properties.
301    /// Thin shim over [`Window::set_virtual_buffer_content`].
302    pub fn set_virtual_buffer_content(
303        &mut self,
304        buffer_id: BufferId,
305        entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
306    ) -> Result<(), String> {
307        self.active_window_mut()
308            .set_virtual_buffer_content(buffer_id, entries)
309    }
310}
311
312impl crate::app::window::Window {
313    /// Create a virtual buffer without attaching it to any split's tab list.
314    ///
315    /// Like [`Self::create_virtual_buffer`] but does **not** add the new
316    /// buffer to any split's tab list. Use when the caller is going to
317    /// seed a freshly-created split (e.g. the Utility Dock leaf) with
318    /// the new buffer directly — without it, the buffer would briefly
319    /// appear as a phantom tab in the previously-active split, requiring
320    /// a separate cleanup pass to remove it.
321    pub fn create_virtual_buffer_detached(
322        &mut self,
323        name: String,
324        mode: String,
325        read_only: bool,
326    ) -> BufferId {
327        let buffer_id = self.alloc_buffer_id();
328
329        let mut state = EditorState::new(
330            self.terminal_width,
331            self.terminal_height,
332            self.config().editor.large_file_threshold_bytes as usize,
333            Arc::clone(&self.authority().filesystem),
334        );
335        state.set_language_from_name(&name, &self.resources.grammar_registry);
336        state
337            .margins
338            .configure_for_line_numbers(self.config().editor.line_numbers);
339
340        self.buffers.insert(buffer_id, state);
341        self.event_logs
342            .insert(buffer_id, crate::model::event::EventLog::new());
343
344        let metadata = crate::app::types::BufferMetadata::virtual_buffer(name, mode, read_only);
345        self.buffer_metadata.insert(buffer_id, metadata);
346
347        buffer_id
348    }
349
350    /// Create a virtual buffer and add it to the active split's tab bar.
351    pub fn create_virtual_buffer(
352        &mut self,
353        name: String,
354        mode: String,
355        read_only: bool,
356    ) -> BufferId {
357        let buffer_id = self.alloc_buffer_id();
358
359        let mut state = EditorState::new(
360            self.terminal_width,
361            self.terminal_height,
362            self.config().editor.large_file_threshold_bytes as usize,
363            Arc::clone(&self.authority().filesystem),
364        );
365        state.set_language_from_name(&name, &self.resources.grammar_registry);
366        state
367            .margins
368            .configure_for_line_numbers(self.config().editor.line_numbers);
369
370        self.buffers.insert(buffer_id, state);
371        self.event_logs
372            .insert(buffer_id, crate::model::event::EventLog::new());
373
374        let metadata = crate::app::types::BufferMetadata::virtual_buffer(name, mode, read_only);
375        self.buffer_metadata.insert(buffer_id, metadata);
376
377        let (mgr, _) = self
378            .buffers
379            .splits()
380            .expect("active window must have a populated split layout");
381        let active_split = mgr.active_split();
382        let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
383        let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
384        let cfg = self.config().editor.clone();
385        let terminal_width = self.terminal_width;
386        let terminal_height = self.terminal_height;
387        if let Some(view_state) = self
388            .split_view_states_mut()
389            .expect("active window must have a populated split layout")
390            .get_mut(&active_split)
391        {
392            view_state.add_buffer(buffer_id);
393            let buf_state = view_state.ensure_buffer_state(buffer_id);
394            buf_state.apply_config_defaults(
395                cfg.line_numbers,
396                cfg.highlight_current_line,
397                line_wrap,
398                cfg.wrap_indent,
399                wrap_column,
400                cfg.rulers.clone(),
401                cfg.scroll_offset,
402            );
403        } else {
404            let mut view_state =
405                SplitViewState::with_buffer(terminal_width, terminal_height, buffer_id);
406            view_state.apply_config_defaults(
407                cfg.line_numbers,
408                cfg.highlight_current_line,
409                line_wrap,
410                cfg.wrap_indent,
411                wrap_column,
412                cfg.rulers,
413                cfg.scroll_offset,
414            );
415            self.split_view_states_mut()
416                .expect("active window must have a populated split layout")
417                .insert(active_split, view_state);
418        }
419
420        buffer_id
421    }
422
423    /// Replace a virtual buffer's content + overlays + cursors clamp.
424    /// Pure window-state mutation: rewrites the buffer text, clears
425    /// and re-installs overlays for the new content, and clamps every
426    /// per-split cursor showing this buffer to a char boundary in
427    /// the new length. Returns `Err` when the buffer is missing.
428    pub fn set_virtual_buffer_content(
429        &mut self,
430        buffer_id: BufferId,
431        entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
432    ) -> Result<(), String> {
433        let state = self
434            .buffers
435            .get_mut(&buffer_id)
436            .ok_or_else(|| "Buffer not found".to_string())?;
437
438        // Build text and properties from entries
439        let (text, properties, collected_overlays) =
440            crate::primitives::text_property::TextPropertyManager::from_entries(entries);
441
442        // Replace buffer content
443        // Note: we use buffer.delete_bytes/insert directly (not state.delete_range/insert_text_at)
444        // which bypasses marker_list adjustment. Clear ALL overlays first so no stale markers
445        // remain pointing at invalid positions in the new content.
446        state.overlays.clear(&mut state.marker_list);
447
448        let current_len = state.buffer.len();
449        if current_len > 0 {
450            state.buffer.delete_bytes(0, current_len);
451        }
452        state.buffer.insert(0, &text);
453
454        // Clear modified flag since this is virtual buffer content setting, not user edits
455        state.buffer.clear_modified();
456
457        // Set text properties
458        state.text_properties = properties;
459
460        // Create inline overlays for the new content. Build the full vec
461        // first and bulk-add it so the OverlayManager sorts exactly once;
462        // a per-overlay `add` re-sorts every time and is O(n² log n) for
463        // N entries (a big git-show diff can be ~500k overlays).
464        {
465            use crate::view::overlay::{Overlay, OverlayFace};
466            use fresh_core::overlay::OverlayNamespace;
467
468            let inline_ns = OverlayNamespace::from_string("_inline".to_string());
469            let mut new_overlays = Vec::with_capacity(collected_overlays.len());
470
471            for co in collected_overlays {
472                let face = OverlayFace::from_options(&co.options);
473                let mut overlay = Overlay::with_namespace(
474                    &mut state.marker_list,
475                    co.range,
476                    face,
477                    inline_ns.clone(),
478                );
479                overlay.extend_to_line_end = co.options.extend_to_line_end;
480                if let Some(url) = co.options.url {
481                    overlay.url = Some(url);
482                }
483                new_overlays.push(overlay);
484            }
485            state.overlays.extend(new_overlays);
486        }
487
488        // Each split keeps its own cursor; just clamp anything that fell
489        // past the new buffer end and snap to a char boundary. Don't read
490        // one split's cursor and write it into the others.
491        self.buffers
492            .with_buffer_and_view_states(buffer_id, |state, vs_map| {
493                let new_len = state.buffer.len();
494                let buffer = &state.buffer;
495                for view_state in vs_map.values_mut() {
496                    let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) else {
497                        continue;
498                    };
499                    buf_state.cursors.map(|cursor| {
500                        let pos = cursor.position.min(new_len);
501                        cursor.position = buffer.snap_to_char_boundary(pos);
502                        if let Some(anchor) = cursor.anchor {
503                            let clamped = anchor.min(new_len);
504                            cursor.anchor = Some(buffer.snap_to_char_boundary(clamped));
505                        }
506                    });
507                }
508            });
509        Ok(())
510    }
511}