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