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