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.authority.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.authority.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            .authority
156            .filesystem
157            .metadata(&temp_path)
158            .map(|m| m.size as usize)
159            .unwrap_or(last_known);
160
161        // If file grew, extend the buffer
162        if self.stdin_stream.record_growth(current_size) {
163            if let Some(editor_state) = self.buffers.get_mut(&buffer_id) {
164                editor_state
165                    .buffer
166                    .extend_streaming(&temp_path, current_size);
167            }
168            self.status_message =
169                Some(t!("stdin.streaming_bytes", bytes = current_size).to_string());
170            changed = true;
171        }
172
173        // Drain a just-finished thread and surface its outcome to the user.
174        if let Some(outcome) = self.stdin_stream.take_finished_thread_outcome() {
175            match outcome {
176                ThreadOutcome::Success => {
177                    tracing::info!("Stdin streaming completed successfully");
178                }
179                ThreadOutcome::Error(msg) => {
180                    tracing::warn!("Stdin streaming error: {}", msg);
181                    self.status_message = Some(t!("stdin.read_error", error = msg).to_string());
182                }
183                ThreadOutcome::Panic => {
184                    tracing::warn!("Stdin streaming thread panicked");
185                    self.status_message = Some(t!("stdin.read_error_panic").to_string());
186                }
187            }
188            self.complete_stdin_streaming();
189            changed = true;
190        }
191
192        changed
193    }
194
195    /// Mark stdin streaming as complete.
196    /// Called when the background thread finishes.
197    pub fn complete_stdin_streaming(&mut self) {
198        let Some(buffer_id) = self.stdin_stream.buffer_id() else {
199            return;
200        };
201        let Some(temp_path) = self.stdin_stream.temp_path().map(Path::to_path_buf) else {
202            return;
203        };
204
205        self.stdin_stream.mark_complete();
206
207        // Final poll to get any remaining data
208        let final_size = self
209            .authority
210            .filesystem
211            .metadata(&temp_path)
212            .map(|m| m.size as usize)
213            .unwrap_or(self.stdin_stream.last_known_size());
214
215        if self.stdin_stream.record_growth(final_size) {
216            if let Some(editor_state) = self.buffers.get_mut(&buffer_id) {
217                editor_state.buffer.extend_streaming(&temp_path, final_size);
218            }
219        }
220
221        self.status_message = Some(
222            t!(
223                "stdin.read_complete",
224                bytes = self.stdin_stream.last_known_size()
225            )
226            .to_string(),
227        );
228    }
229
230    /// Check if stdin streaming is active (not complete).
231    pub fn is_stdin_streaming(&self) -> bool {
232        self.stdin_stream.is_active()
233    }
234
235    /// Create a new virtual buffer (not backed by a file)
236    ///
237    /// # Arguments
238    /// * `name` - Display name (e.g., "*Diagnostics*")
239    /// * `mode` - Buffer mode for keybindings (e.g., "diagnostics-list")
240    /// * `read_only` - Whether the buffer should be read-only
241    ///
242    /// # Returns
243    /// The BufferId of the created virtual buffer
244    ///
245    /// Like [`Self::create_virtual_buffer`] but does **not** add the
246    /// new buffer to any split's tab list. Use this when the caller
247    /// is going to seed a freshly-created split (e.g. the Utility
248    /// Dock leaf) with the new buffer directly — without it, the
249    /// buffer would briefly appear as a phantom tab in whatever the
250    /// previously-active split was, requiring a separate cleanup
251    /// pass to remove it.
252    pub fn create_virtual_buffer_detached(
253        &mut self,
254        name: String,
255        mode: String,
256        read_only: bool,
257    ) -> BufferId {
258        let buffer_id = BufferId(self.next_buffer_id);
259        self.next_buffer_id += 1;
260
261        let mut state = EditorState::new(
262            self.terminal_width,
263            self.terminal_height,
264            self.config.editor.large_file_threshold_bytes as usize,
265            Arc::clone(&self.authority.filesystem),
266        );
267        // Set syntax highlighting based on buffer name (e.g., "*OURS*.c"
268        // gets C highlighting). Mirrors create_virtual_buffer.
269        state.set_language_from_name(&name, &self.grammar_registry);
270        state
271            .margins
272            .configure_for_line_numbers(self.config.editor.line_numbers);
273
274        self.buffers.insert(buffer_id, state);
275        self.event_logs
276            .insert(buffer_id, crate::model::event::EventLog::new());
277
278        // Set virtual buffer metadata
279        let metadata = super::types::BufferMetadata::virtual_buffer(name, mode, read_only);
280        self.buffer_metadata.insert(buffer_id, metadata);
281
282        buffer_id
283    }
284
285    pub fn create_virtual_buffer(
286        &mut self,
287        name: String,
288        mode: String,
289        read_only: bool,
290    ) -> BufferId {
291        let buffer_id = BufferId(self.next_buffer_id);
292        self.next_buffer_id += 1;
293
294        let mut state = EditorState::new(
295            self.terminal_width,
296            self.terminal_height,
297            self.config.editor.large_file_threshold_bytes as usize,
298            Arc::clone(&self.authority.filesystem),
299        );
300        // Note: line_wrap_enabled is set on SplitViewState.viewport when the split is created
301
302        // Set syntax highlighting based on buffer name (e.g., "*OURS*.c" will get C highlighting)
303        state.set_language_from_name(&name, &self.grammar_registry);
304
305        // Apply line_numbers default from config
306        state
307            .margins
308            .configure_for_line_numbers(self.config.editor.line_numbers);
309
310        self.buffers.insert(buffer_id, state);
311        self.event_logs
312            .insert(buffer_id, crate::model::event::EventLog::new());
313
314        // Set virtual buffer metadata
315        let metadata = super::types::BufferMetadata::virtual_buffer(name, mode, read_only);
316        self.buffer_metadata.insert(buffer_id, metadata);
317
318        // Add buffer to the active split's open_buffers (tabs)
319        let active_split = self.split_manager.active_split();
320        let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
321        let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
322        if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
323            view_state.add_buffer(buffer_id);
324            let buf_state = view_state.ensure_buffer_state(buffer_id);
325            buf_state.apply_config_defaults(
326                self.config.editor.line_numbers,
327                self.config.editor.highlight_current_line,
328                line_wrap,
329                self.config.editor.wrap_indent,
330                wrap_column,
331                self.config.editor.rulers.clone(),
332            );
333        } else {
334            // Create view state if it doesn't exist
335            let mut view_state =
336                SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buffer_id);
337            view_state.apply_config_defaults(
338                self.config.editor.line_numbers,
339                self.config.editor.highlight_current_line,
340                line_wrap,
341                self.config.editor.wrap_indent,
342                wrap_column,
343                self.config.editor.rulers.clone(),
344            );
345            self.split_view_states.insert(active_split, view_state);
346        }
347
348        buffer_id
349    }
350
351    /// Set the content of a virtual buffer with text properties
352    ///
353    /// # Arguments
354    /// * `buffer_id` - The virtual buffer to update
355    /// * `entries` - Text entries with embedded properties
356    pub fn set_virtual_buffer_content(
357        &mut self,
358        buffer_id: BufferId,
359        entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
360    ) -> Result<(), String> {
361        let state = self
362            .buffers
363            .get_mut(&buffer_id)
364            .ok_or_else(|| "Buffer not found".to_string())?;
365
366        // Build text and properties from entries
367        let (text, properties, collected_overlays) =
368            crate::primitives::text_property::TextPropertyManager::from_entries(entries);
369
370        // Replace buffer content
371        // Note: we use buffer.delete_bytes/insert directly (not state.delete_range/insert_text_at)
372        // which bypasses marker_list adjustment. Clear ALL overlays first so no stale markers
373        // remain pointing at invalid positions in the new content.
374        state.overlays.clear(&mut state.marker_list);
375
376        let current_len = state.buffer.len();
377        if current_len > 0 {
378            state.buffer.delete_bytes(0, current_len);
379        }
380        state.buffer.insert(0, &text);
381
382        // Clear modified flag since this is virtual buffer content setting, not user edits
383        state.buffer.clear_modified();
384
385        // Set text properties
386        state.text_properties = properties;
387
388        // Create inline overlays for the new content. Build the full vec
389        // first and bulk-add it so the OverlayManager sorts exactly once;
390        // a per-overlay `add` re-sorts every time and is O(n² log n) for
391        // N entries (a big git-show diff can be ~500k overlays).
392        {
393            use crate::view::overlay::{Overlay, OverlayFace};
394            use fresh_core::overlay::OverlayNamespace;
395
396            let inline_ns = OverlayNamespace::from_string("_inline".to_string());
397            let mut new_overlays = Vec::with_capacity(collected_overlays.len());
398
399            for co in collected_overlays {
400                let face = OverlayFace::from_options(&co.options);
401                let mut overlay = Overlay::with_namespace(
402                    &mut state.marker_list,
403                    co.range,
404                    face,
405                    inline_ns.clone(),
406                );
407                overlay.extend_to_line_end = co.options.extend_to_line_end;
408                if let Some(url) = co.options.url {
409                    overlay.url = Some(url);
410                }
411                new_overlays.push(overlay);
412            }
413            state.overlays.extend(new_overlays);
414        }
415
416        // Each split keeps its own cursor; just clamp anything that fell
417        // past the new buffer end and snap to a char boundary. Don't read
418        // one split's cursor and write it into the others.
419        let new_len = state.buffer.len();
420        // `state` is no longer used past this point — re-borrow `self.buffers`
421        // immutably for the snap and `self.split_view_states` mutably for the
422        // write. These are disjoint fields of `self`.
423        let buffer = &self
424            .buffers
425            .get(&buffer_id)
426            .expect("buffer still present")
427            .buffer;
428        for view_state in self.split_view_states.values_mut() {
429            let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) else {
430                continue;
431            };
432            buf_state.cursors.map(|cursor| {
433                let pos = cursor.position.min(new_len);
434                cursor.position = buffer.snap_to_char_boundary(pos);
435                if let Some(anchor) = cursor.anchor {
436                    let clamped = anchor.min(new_len);
437                    cursor.anchor = Some(buffer.snap_to_char_boundary(clamped));
438                }
439            });
440        }
441        Ok(())
442    }
443}