venus_core/graph/
source_editor.rs

1//! Source file editor for inserting, deleting, and reordering cells in .rs notebook files.
2//!
3//! Uses advisory file locking to prevent race conditions when multiple processes
4//! modify the same notebook file concurrently.
5
6use std::collections::HashSet;
7use std::fs::{self, File};
8use std::path::{Path, PathBuf};
9use fs2::FileExt;
10
11use syn::spanned::Spanned;
12use syn::{Attribute, File as SynFile};
13
14use serde::{Deserialize, Serialize};
15
16use crate::error::{Error, Result};
17
18/// Direction for moving a cell.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum MoveDirection {
22    /// Move cell up (swap with previous cell).
23    Up,
24    /// Move cell down (swap with next cell).
25    Down,
26}
27
28/// Editor for modifying .rs notebook source files.
29///
30/// Holds an exclusive file lock for the duration of the edit session
31/// to prevent concurrent modifications.
32pub struct SourceEditor {
33    /// Path to the source file.
34    path: PathBuf,
35    /// Current file content.
36    content: String,
37    /// Lock file handle (held for edit duration)
38    _lock_file: Option<File>,
39}
40
41impl SourceEditor {
42    /// Load a source file for editing.
43    ///
44    /// Acquires an exclusive advisory lock on the file to prevent
45    /// concurrent modifications from other processes.
46    pub fn load(path: &Path) -> Result<Self> {
47        // Open file for reading with exclusive lock
48        let lock_file = File::open(path)?;
49
50        // Try to acquire exclusive lock (non-blocking)
51        lock_file.try_lock_exclusive().map_err(|e| {
52            Error::Io(std::io::Error::new(
53                std::io::ErrorKind::WouldBlock,
54                format!("File is locked by another process: {}: {}", path.display(), e),
55            ))
56        })?;
57
58        let content = fs::read_to_string(path)?;
59
60        Ok(Self {
61            path: path.to_path_buf(),
62            content,
63            _lock_file: Some(lock_file),
64        })
65    }
66
67    /// Insert a new cell after the specified cell.
68    ///
69    /// If `after_cell_id` is None, inserts at the end of the file.
70    /// Returns the name of the newly created cell.
71    pub fn insert_cell(&mut self, after_cell_id: Option<&str>) -> Result<String> {
72        // Parse the file to find cell positions and existing names
73        let file: SynFile = syn::parse_str(&self.content)
74            .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
75
76        // Collect existing cell names for unique name generation
77        let existing_names = self.collect_cell_names(&file);
78
79        // Generate a unique name for the new cell
80        let new_name = self.generate_unique_name(&existing_names);
81
82        // Find the position to insert the new cell
83        let insert_pos = self.find_insert_position(&file, after_cell_id)?;
84
85        // Generate the cell code
86        let cell_code = self.generate_cell_code(&new_name);
87
88        // Insert the cell code at the position
89        self.content.insert_str(insert_pos, &cell_code);
90
91        Ok(new_name)
92    }
93
94    /// Delete a cell by name.
95    ///
96    /// Returns the name of the deleted cell.
97    pub fn delete_cell(&mut self, cell_name: &str) -> Result<String> {
98        // Parse the file to find cell positions
99        let file: SynFile = syn::parse_str(&self.content)
100            .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
101
102        // Find the cell's span (including doc comments and attributes)
103        let (start_line, end_line) = self.find_cell_span(&file, cell_name)?;
104
105        // Convert line numbers to byte offsets
106        let lines: Vec<&str> = self.content.lines().collect();
107        let start_offset = self.line_start_offset(start_line, &lines);
108        let end_offset = self.line_to_byte_offset(end_line, &lines);
109
110        // Remove the cell from content
111        self.content = format!(
112            "{}{}",
113            &self.content[..start_offset],
114            &self.content[end_offset..]
115        );
116
117        // Clean up extra blank lines
118        self.cleanup_blank_lines();
119
120        Ok(cell_name.to_string())
121    }
122
123    /// Duplicate a cell by name.
124    ///
125    /// Creates a copy of the cell with a unique name (e.g., `cell_name_copy`).
126    /// The new cell is inserted immediately after the original.
127    /// Returns the name of the new cell.
128    pub fn duplicate_cell(&mut self, cell_name: &str) -> Result<String> {
129        // Parse the file to find cell positions and existing names
130        let file: SynFile = syn::parse_str(&self.content)
131            .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
132
133        // Find the cell's span
134        let (start_line, end_line) = self.find_cell_span(&file, cell_name)?;
135
136        // Collect existing names to generate unique copy name
137        let existing_names = self.collect_cell_names(&file);
138
139        // Generate a unique name for the copy
140        let new_name = self.generate_copy_name(cell_name, &existing_names);
141
142        // Extract the cell's source code
143        let lines: Vec<&str> = self.content.lines().collect();
144        let start_offset = self.line_start_offset(start_line, &lines);
145        let end_offset = self.line_to_byte_offset(end_line, &lines);
146        let cell_source = &self.content[start_offset..end_offset];
147
148        // Replace the function name in the duplicated code
149        let new_cell_source = cell_source.replace(
150            &format!("fn {}(", cell_name),
151            &format!("fn {}(", new_name),
152        );
153
154        // Insert the new cell after the original
155        let insert_code = format!("\n{}", new_cell_source);
156        self.content.insert_str(end_offset, &insert_code);
157
158        Ok(new_name)
159    }
160
161    /// Move a cell up or down by swapping with its neighbor.
162    ///
163    /// Returns Ok(()) on success.
164    pub fn move_cell(&mut self, cell_name: &str, direction: MoveDirection) -> Result<()> {
165        // Parse the file to find all cells in order
166        let file: SynFile = syn::parse_str(&self.content)
167            .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
168
169        // Collect all cells with their spans in order
170        let cells = self.collect_cell_spans(&file);
171
172        // Find the target cell's index
173        let cell_idx = cells
174            .iter()
175            .position(|(name, _, _)| name == cell_name)
176            .ok_or_else(|| Error::CellNotFound(format!("Cell '{}' not found", cell_name)))?;
177
178        // Find the neighbor to swap with
179        let neighbor_idx = match direction {
180            MoveDirection::Up => {
181                if cell_idx == 0 {
182                    return Err(Error::InvalidOperation("Cannot move first cell up".to_string()));
183                }
184                cell_idx - 1
185            }
186            MoveDirection::Down => {
187                if cell_idx >= cells.len() - 1 {
188                    return Err(Error::InvalidOperation("Cannot move last cell down".to_string()));
189                }
190                cell_idx + 1
191            }
192        };
193
194        // Get spans for both cells (ensure first is before second)
195        let (first_idx, second_idx) = if cell_idx < neighbor_idx {
196            (cell_idx, neighbor_idx)
197        } else {
198            (neighbor_idx, cell_idx)
199        };
200
201        let (_, first_start, first_end) = cells[first_idx];
202        let (_, second_start, second_end) = cells[second_idx];
203
204        // Extract source code for both cells
205        let lines: Vec<&str> = self.content.lines().collect();
206        let first_start_offset = self.line_start_offset(first_start, &lines);
207        let first_end_offset = self.line_to_byte_offset(first_end, &lines);
208        let second_start_offset = self.line_start_offset(second_start, &lines);
209        let second_end_offset = self.line_to_byte_offset(second_end, &lines);
210
211        let first_source = self.content[first_start_offset..first_end_offset].to_string();
212        let second_source = self.content[second_start_offset..second_end_offset].to_string();
213
214        // Build new content by replacing both cells in reverse order (to preserve offsets)
215        let mut new_content = String::new();
216        new_content.push_str(&self.content[..first_start_offset]);
217        new_content.push_str(&second_source);
218        new_content.push_str(&self.content[first_end_offset..second_start_offset]);
219        new_content.push_str(&first_source);
220        new_content.push_str(&self.content[second_end_offset..]);
221
222        self.content = new_content;
223
224        Ok(())
225    }
226
227    /// Rename a cell's display name by updating its doc comment.
228    ///
229    /// Updates or adds a `# Display Name` heading to the cell's doc comment.
230    pub fn rename_cell(&mut self, cell_name: &str, new_display_name: &str) -> Result<()> {
231        // Parse the file to find the cell
232        let file: SynFile = syn::parse_str(&self.content)
233            .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
234
235        // Find the cell
236        for item in &file.items {
237            if let syn::Item::Fn(func) = item
238                && Self::has_cell_attribute(&func.attrs) {
239                    let name = func.sig.ident.to_string();
240                    if name == cell_name {
241                        // Extract existing doc comments (excluding # heading lines)
242                        let mut doc_lines: Vec<String> = Vec::new();
243
244                        for attr in &func.attrs {
245                            if attr.path().is_ident("doc")
246                                && let syn::Meta::NameValue(nv) = &attr.meta
247                                && let syn::Expr::Lit(syn::ExprLit {
248                                    lit: syn::Lit::Str(s),
249                                    ..
250                                }) = &nv.value
251                            {
252                                let line = s.value();
253                                let trimmed = line.trim_start();
254
255                                // Skip existing # heading (we'll add new one)
256                                if trimmed.starts_with('#') {
257                                    continue;
258                                }
259
260                                doc_lines.push(line);
261                            }
262                        }
263
264                        // Build new doc comment with display name heading
265                        let mut new_doc_lines = vec![format!("# {}", new_display_name)];
266                        if !doc_lines.is_empty() {
267                            // Add blank line between heading and description
268                            new_doc_lines.push(String::new());
269                            new_doc_lines.extend(doc_lines);
270                        }
271
272                        // Find the span for doc comments and attributes
273                        let doc_start_line = if !func.attrs.is_empty() {
274                            func.attrs
275                                .iter()
276                                .filter(|a| a.path().is_ident("doc"))
277                                .map(|a| a.span().start().line)
278                                .min()
279                                .unwrap_or(func.attrs[0].span().start().line)
280                        } else {
281                            func.span().start().line
282                        };
283
284                        // Find the function declaration line (pub fn ...)
285                        let fn_start_line = func.sig.fn_token.span.start().line;
286
287                        // Reconstruct the cell with new doc comments
288                        let lines: Vec<&str> = self.content.lines().collect();
289
290                        // Get the indentation of the original doc comments or function
291                        let indent = if !func.attrs.is_empty() {
292                            Self::get_line_indent(&lines, doc_start_line)
293                        } else {
294                            Self::get_line_indent(&lines, fn_start_line)
295                        };
296
297                        // Build new doc comment block
298                        let new_doc_comment = new_doc_lines
299                            .iter()
300                            .map(|line| format!("{}/// {}", indent, line))
301                            .collect::<Vec<_>>()
302                            .join("\n");
303
304                        // Find where to replace
305                        let replace_start = self.line_start_offset(doc_start_line, &lines);
306                        let replace_end = self.line_start_offset(fn_start_line, &lines);
307
308                        // Build new content
309                        let mut new_content = String::new();
310                        new_content.push_str(&self.content[..replace_start]);
311                        new_content.push_str(&new_doc_comment);
312                        new_content.push('\n');
313
314                        // Add the #[venus::cell] attribute if it's not a doc comment
315                        let mut added_cell_attr = false;
316                        for attr in &func.attrs {
317                            if !attr.path().is_ident("doc")
318                                && !added_cell_attr {
319                                    new_content.push_str(&format!("{}#[venus::cell]\n", indent));
320                                    added_cell_attr = true;
321                                }
322                        }
323
324                        if !added_cell_attr {
325                            new_content.push_str(&format!("{}#[venus::cell]\n", indent));
326                        }
327
328                        new_content.push_str(&self.content[replace_end..]);
329
330                        self.content = new_content;
331                        return Ok(());
332                    }
333                }
334        }
335
336        Err(Error::CellNotFound(format!("Cell '{}' not found", cell_name)))
337    }
338
339    /// Insert a markdown cell at a specific line position.
340    ///
341    /// If `after_line` is None, inserts at the beginning of the file.
342    /// Content should be plain markdown text (without `///` prefix).
343    pub fn insert_markdown_cell(&mut self, content: &str, after_line: Option<usize>) -> Result<()> {
344        let lines: Vec<&str> = self.content.lines().collect();
345
346        // Format content as regular comment block (//)
347        let markdown_block = content
348            .lines()
349            .map(|line| format!("// {}", line))
350            .collect::<Vec<_>>()
351            .join("\n");
352
353        // Determine insertion point
354        let insert_offset = if let Some(line_num) = after_line {
355            if line_num > lines.len() {
356                self.content.len()
357            } else {
358                // Find the end of the function/block at this line
359                // We need to skip past the entire function body to insert after it
360                self.find_block_end(line_num, &lines)
361            }
362        } else {
363            0 // Insert at beginning
364        };
365
366        // Insert markdown block with appropriate spacing
367        let insert_text = if insert_offset == 0 {
368            format!("{}\n\n", markdown_block)
369        } else {
370            format!("\n\n{}\n", markdown_block)
371        };
372
373        self.content.insert_str(insert_offset, &insert_text);
374
375        Ok(())
376    }
377
378    /// Insert raw Rust code (for definition cells, imports, etc.) without any formatting.
379    /// This is a generic method for inserting plain Rust code without comment prefix or attributes.
380    pub fn insert_raw_code(&mut self, content: &str, after_line: Option<usize>) -> Result<()> {
381        let lines: Vec<&str> = self.content.lines().collect();
382
383        // Determine insertion point
384        let insert_offset = if let Some(line_num) = after_line {
385            if line_num > lines.len() {
386                self.content.len()
387            } else {
388                // Find the end of the function/block at this line
389                self.find_block_end(line_num, &lines)
390            }
391        } else {
392            0 // Insert at beginning
393        };
394
395        // Insert raw code with appropriate spacing (NO formatting)
396        let insert_text = if insert_offset == 0 {
397            format!("{}\n\n", content)
398        } else {
399            format!("\n\n{}\n", content)
400        };
401
402        self.content.insert_str(insert_offset, &insert_text);
403
404        Ok(())
405    }
406
407    /// Find the byte offset after the closing brace of a block starting at the given line.
408    fn find_block_end(&self, start_line: usize, lines: &[&str]) -> usize {
409        if start_line == 0 || start_line > lines.len() {
410            return self.content.len();
411        }
412
413        let mut brace_depth = 0;
414        let mut found_opening = false;
415        let mut offset = 0;
416
417        for (i, line) in lines.iter().enumerate() {
418            let line_num = i + 1;
419
420            // Calculate offset for this line
421            if line_num < start_line {
422                offset += line.len() + 1; // +1 for newline
423                continue;
424            }
425
426            // Count braces
427            for ch in line.chars() {
428                offset += ch.len_utf8();
429                match ch {
430                    '{' => {
431                        brace_depth += 1;
432                        found_opening = true;
433                    }
434                    '}' => {
435                        brace_depth -= 1;
436                        // If we're back to 0 and we found an opening brace, we're done
437                        if found_opening && brace_depth == 0 {
438                            offset += 1; // Include the newline after closing brace
439                            return offset.min(self.content.len());
440                        }
441                    }
442                    _ => {}
443                }
444            }
445
446            offset += 1; // newline
447        }
448
449        // If we didn't find a complete block, return end of content
450        self.content.len()
451    }
452
453    /// Edit an existing markdown cell by line range.
454    ///
455    /// Replaces the comment block at the given line range with new content.
456    /// If `is_module_doc` is true, uses `//!` syntax; otherwise uses `///`.
457    pub fn edit_markdown_cell(&mut self, start_line: usize, end_line: usize, new_content: &str, is_module_doc: bool) -> Result<()> {
458        let lines: Vec<&str> = self.content.lines().collect();
459
460        if start_line == 0 || start_line > lines.len() || end_line > lines.len() || start_line > end_line {
461            return Err(Error::InvalidOperation(format!(
462                "Invalid line range: {}-{}",
463                start_line, end_line
464            )));
465        }
466
467        // Format new content as comment block (either //! or ///)
468        let comment_prefix = if is_module_doc { "//!" } else { "///" };
469        let markdown_block = new_content
470            .lines()
471            .map(|line| format!("{} {}", comment_prefix, line))
472            .collect::<Vec<_>>()
473            .join("\n");
474
475        // Calculate byte offsets
476        let start_offset = self.line_start_offset(start_line, &lines);
477        let end_offset = self.line_to_byte_offset(end_line, &lines);
478
479        // Replace the old block with new content
480        // Note: end_offset already points past the newline of end_line
481        // and markdown_block has internal newlines but no trailing newline
482        let needs_newline = end_offset < self.content.len();
483        self.content = if needs_newline {
484            format!(
485                "{}{}\n{}",
486                &self.content[..start_offset],
487                markdown_block,
488                &self.content[end_offset..]
489            )
490        } else {
491            // Last line of file - no trailing newline needed
492            format!(
493                "{}{}",
494                &self.content[..start_offset],
495                markdown_block
496            )
497        };
498
499        eprintln!("  needs_newline={}", needs_newline);
500
501        Ok(())
502    }
503
504    /// Edit raw Rust code by line range (for definition cells, etc.) without any formatting.
505    /// Replaces the code block at the given line range with new content as-is.
506    pub fn edit_raw_code(&mut self, start_line: usize, end_line: usize, new_content: &str) -> Result<()> {
507        let lines: Vec<&str> = self.content.lines().collect();
508
509        if start_line == 0 || start_line > lines.len() || end_line > lines.len() || start_line > end_line {
510            return Err(Error::InvalidOperation(format!(
511                "Invalid line range: {}-{}",
512                start_line, end_line
513            )));
514        }
515
516        // Calculate byte offsets
517        let start_offset = self.line_start_offset(start_line, &lines);
518        let end_offset = self.line_to_byte_offset(end_line, &lines);
519
520        // Replace with raw content (no formatting)
521        let needs_newline = end_offset < self.content.len();
522        self.content = if needs_newline {
523            format!(
524                "{}{}\n{}",
525                &self.content[..start_offset],
526                new_content,
527                &self.content[end_offset..]
528            )
529        } else {
530            format!(
531                "{}{}",
532                &self.content[..start_offset],
533                new_content
534            )
535        };
536
537        Ok(())
538    }
539
540    /// Delete a markdown cell by line range.
541    pub fn delete_markdown_cell(&mut self, start_line: usize, end_line: usize) -> Result<()> {
542        let lines: Vec<&str> = self.content.lines().collect();
543
544        if start_line == 0 || start_line > lines.len() || end_line > lines.len() || start_line > end_line {
545            return Err(Error::InvalidOperation(format!(
546                "Invalid line range: {}-{}",
547                start_line, end_line
548            )));
549        }
550
551        // Calculate byte offsets
552        let start_offset = self.line_start_offset(start_line, &lines);
553        let end_offset = self.line_to_byte_offset(end_line, &lines);
554
555        // Remove the markdown block
556        self.content = format!(
557            "{}{}",
558            &self.content[..start_offset],
559            &self.content[end_offset..]
560        );
561
562        // Clean up extra blank lines
563        self.cleanup_blank_lines();
564
565        Ok(())
566    }
567
568    /// Move a markdown cell up or down.
569    ///
570    /// Swaps the markdown block with the adjacent one.
571    pub fn move_markdown_cell(
572        &mut self,
573        start_line: usize,
574        end_line: usize,
575        direction: MoveDirection,
576    ) -> Result<()> {
577        let lines: Vec<&str> = self.content.lines().collect();
578
579        if start_line == 0 || start_line > lines.len() || end_line > lines.len() || start_line > end_line {
580            return Err(Error::InvalidOperation(format!(
581                "Invalid line range: {}-{}",
582                start_line, end_line
583            )));
584        }
585
586        // Extract the markdown block
587        let start_offset = self.line_start_offset(start_line, &lines);
588        let end_offset = self.line_to_byte_offset(end_line, &lines);
589        let markdown_block = self.content[start_offset..end_offset].to_string();
590
591        // Find the adjacent block to swap with
592        let (swap_start_line, swap_end_line) = match direction {
593            MoveDirection::Up => {
594                // Find the previous block (scan backwards)
595                if start_line == 1 {
596                    return Err(Error::InvalidOperation("Cannot move first block up".to_string()));
597                }
598
599                // Simple heuristic: find previous non-empty line group
600                let mut search_line = start_line - 1;
601                while search_line > 0 && lines[search_line - 1].trim().is_empty() {
602                    search_line -= 1;
603                }
604
605                if search_line == 0 {
606                    return Err(Error::InvalidOperation("No block found above".to_string()));
607                }
608
609                // Find the start of this block
610                let mut block_start = search_line;
611                while block_start > 1 && !lines[block_start - 2].trim().is_empty() {
612                    block_start -= 1;
613                }
614
615                (block_start, search_line)
616            }
617            MoveDirection::Down => {
618                // Find the next block (scan forwards)
619                if end_line >= lines.len() {
620                    return Err(Error::InvalidOperation("Cannot move last block down".to_string()));
621                }
622
623                // Skip blank lines
624                let mut search_line = end_line + 1;
625                while search_line <= lines.len() && lines[search_line - 1].trim().is_empty() {
626                    search_line += 1;
627                }
628
629                if search_line > lines.len() {
630                    return Err(Error::InvalidOperation("No block found below".to_string()));
631                }
632
633                // Find the end of this block
634                let block_start = search_line;
635                let mut block_end = search_line;
636                while block_end < lines.len() && !lines[block_end].trim().is_empty() {
637                    block_end += 1;
638                }
639
640                (block_start, block_end)
641            }
642        };
643
644        // Extract the swap block
645        let swap_start_offset = self.line_start_offset(swap_start_line, &lines);
646        let swap_end_offset = self.line_to_byte_offset(swap_end_line, &lines);
647        let swap_block = self.content[swap_start_offset..swap_end_offset].to_string();
648
649        // Perform the swap based on direction
650        match direction {
651            MoveDirection::Up => {
652                // Swap block goes after markdown block
653                self.content = format!(
654                    "{}{}{}{}{}",
655                    &self.content[..swap_start_offset],
656                    &markdown_block,
657                    &self.content[swap_end_offset..start_offset],
658                    &swap_block,
659                    &self.content[end_offset..]
660                );
661            }
662            MoveDirection::Down => {
663                // Markdown block goes after swap block
664                self.content = format!(
665                    "{}{}{}{}{}",
666                    &self.content[..start_offset],
667                    &swap_block,
668                    &self.content[end_offset..swap_start_offset],
669                    &markdown_block,
670                    &self.content[swap_end_offset..]
671                );
672            }
673        }
674
675        Ok(())
676    }
677
678    /// Save changes to the file.
679    ///
680    /// The exclusive lock is maintained until SourceEditor is dropped,
681    /// ensuring no other process can modify the file between save and drop.
682    pub fn save(&self) -> Result<()> {
683        fs::write(&self.path, &self.content)?;
684        // Lock is automatically released when SourceEditor is dropped
685        Ok(())
686    }
687
688    /// Get the source code of a cell (including doc comments and attributes).
689    ///
690    /// Used for undo operations to capture cell content before deletion.
691    pub fn get_cell_source(&self, cell_name: &str) -> Result<String> {
692        let file: SynFile = syn::parse_str(&self.content)
693            .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
694
695        let (start_line, end_line) = self.find_cell_span(&file, cell_name)?;
696
697        let lines: Vec<&str> = self.content.lines().collect();
698        let start_offset = self.line_start_offset(start_line, &lines);
699        let end_offset = self.line_to_byte_offset(end_line, &lines);
700
701        Ok(self.content[start_offset..end_offset].to_string())
702    }
703
704    /// Get the name of the cell that appears before the specified cell.
705    ///
706    /// Returns None if the cell is the first one.
707    /// Used for undo operations to track position for restoration.
708    pub fn get_previous_cell_name(&self, cell_name: &str) -> Result<Option<String>> {
709        let file: SynFile = syn::parse_str(&self.content)
710            .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
711
712        let cells = self.collect_cell_spans(&file);
713
714        let cell_idx = cells
715            .iter()
716            .position(|(name, _, _)| name == cell_name)
717            .ok_or_else(|| Error::CellNotFound(format!("Cell '{}' not found", cell_name)))?;
718
719        if cell_idx == 0 {
720            Ok(None)
721        } else {
722            Ok(Some(cells[cell_idx - 1].0.clone()))
723        }
724    }
725
726    /// Restore a cell with specific source code after a specific cell.
727    ///
728    /// If `after_cell_name` is None, inserts at the beginning (before all cells).
729    /// Used for undo delete operations.
730    pub fn restore_cell(&mut self, source: &str, after_cell_name: Option<&str>) -> Result<()> {
731        let file: SynFile = syn::parse_str(&self.content)
732            .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
733
734        let insert_pos = if let Some(after_name) = after_cell_name {
735            // Insert after the specified cell
736            self.find_insert_position(&file, Some(after_name))?
737        } else {
738            // Insert at the beginning - find the first cell and insert before it
739            let cells = self.collect_cell_spans(&file);
740            if cells.is_empty() {
741                // No cells, insert at end
742                self.content.len()
743            } else {
744                // Insert before the first cell
745                let lines: Vec<&str> = self.content.lines().collect();
746                self.line_start_offset(cells[0].1, &lines)
747            }
748        };
749
750        // Insert the source with appropriate newlines
751        let insert_code = if after_cell_name.is_some() {
752            format!("\n\n{}", source.trim())
753        } else {
754            // Inserting at beginning
755            format!("{}\n\n", source.trim())
756        };
757
758        self.content.insert_str(insert_pos, &insert_code);
759
760        Ok(())
761    }
762
763    /// Find the span of a cell (start line to end line, 1-indexed).
764    /// Includes doc comments and attributes above the function.
765    pub fn find_cell_span(&self, file: &SynFile, cell_name: &str) -> Result<(usize, usize)> {
766        for item in &file.items {
767            if let syn::Item::Fn(func) = item
768                && Self::has_cell_attribute(&func.attrs) {
769                    let name = func.sig.ident.to_string();
770                    if name == cell_name {
771                        // Start from the first attribute or doc comment
772                        let start_line = if !func.attrs.is_empty() {
773                            // Find earliest attribute/doc comment line
774                            func.attrs
775                                .iter()
776                                .map(|a| a.span().start().line)
777                                .min()
778                                .unwrap_or(func.sig.fn_token.span.start().line)
779                        } else {
780                            func.sig.fn_token.span.start().line
781                        };
782
783                        let end_line = func.block.brace_token.span.close().end().line;
784
785                        return Ok((start_line, end_line));
786                    }
787                }
788        }
789
790        Err(Error::CellNotFound(format!("Cell '{}' not found", cell_name)))
791    }
792
793    /// Find just the function span (NOT doc comments) for editing.
794    pub fn find_function_span(&self, file: &SynFile, cell_name: &str) -> Result<(usize, usize)> {
795        for item in &file.items {
796            if let syn::Item::Fn(func) = item
797                && Self::has_cell_attribute(&func.attrs) {
798                    let name = func.sig.ident.to_string();
799                    if name == cell_name {
800                        // Start from pub fn, NOT doc comments
801                        let start_line = func.sig.fn_token.span.start().line;
802                        let end_line = func.block.brace_token.span.close().end().line;
803                        return Ok((start_line, end_line));
804                    }
805                }
806        }
807
808        Err(Error::CellNotFound(format!("Cell '{}' not found", cell_name)))
809    }
810
811    /// Extract existing doc comments for a cell.
812    /// Returns them in "/// comment" format, preserving original formatting.
813    pub fn extract_doc_comments(&self, cell_name: &str) -> Result<Vec<String>> {
814        let file: SynFile = syn::parse_str(&self.content)
815            .map_err(|e| Error::Parse(e.to_string()))?;
816
817        for item in &file.items {
818            if let syn::Item::Fn(func) = item
819                && Self::has_cell_attribute(&func.attrs) {
820                    let name = func.sig.ident.to_string();
821                    if name == cell_name {
822                        let mut doc_lines = Vec::new();
823                        for attr in &func.attrs {
824                            if attr.path().is_ident("doc") {
825                                if let syn::Meta::NameValue(meta) = &attr.meta {
826                                    if let syn::Expr::Lit(lit) = &meta.value {
827                                        if let syn::Lit::Str(s) = &lit.lit {
828                                            // syn stores doc comments without the leading space
829                                            // e.g., /// Hello -> doc = " Hello"
830                                            doc_lines.push(format!("///{}", s.value()));
831                                        }
832                                    }
833                                }
834                            }
835                        }
836                        return Ok(doc_lines);
837                    }
838                }
839        }
840
841        Err(Error::CellNotFound(format!("Cell '{}' not found", cell_name)))
842    }
843
844    /// Reconstruct a complete cell including doc comments and attributes.
845    /// Returns the full cell text: doc comments + #[venus::cell] + function.
846    pub fn reconstruct_cell(&self, cell_name: &str, new_function: &str) -> Result<String> {
847        let doc_comments = self.extract_doc_comments(cell_name)?;
848
849        if !doc_comments.is_empty() {
850            Ok(format!("{}\n#[venus::cell]\n{}", doc_comments.join("\n"), new_function))
851        } else {
852            Ok(format!("#[venus::cell]\n{}", new_function))
853        }
854    }
855
856    /// Reconstruct a cell and get its line span in one call.
857    /// Returns (reconstructed_text, start_line, end_line).
858    pub fn reconstruct_and_get_span(&self, cell_name: &str, new_function: &str) -> Result<(String, usize, usize)> {
859        let file: SynFile = syn::parse_str(&self.content)
860            .map_err(|e| Error::Parse(e.to_string()))?;
861
862        let reconstructed = self.reconstruct_cell(cell_name, new_function)?;
863        let (start_line, end_line) = self.find_cell_span(&file, cell_name)?;
864
865        Ok((reconstructed, start_line, end_line))
866    }
867
868    /// Get the byte offset at the start of a line (1-indexed).
869    fn line_start_offset(&self, line: usize, lines: &[&str]) -> usize {
870        if line <= 1 {
871            return 0;
872        }
873
874        let mut offset = 0;
875        for (i, l) in lines.iter().enumerate() {
876            if i + 1 >= line {
877                break;
878            }
879            offset += l.len();
880            offset += 1; // newline
881        }
882
883        offset.min(self.content.len())
884    }
885
886    /// Get the indentation (leading whitespace) of a line (1-indexed).
887    fn get_line_indent<'a>(lines: &'a [&str], line: usize) -> &'a str {
888        if line == 0 || line > lines.len() {
889            return "";
890        }
891
892        let line_content = lines[line - 1];
893        let trimmed = line_content.trim_start();
894        &line_content[..line_content.len() - trimmed.len()]
895    }
896
897    /// Remove excessive blank lines (more than 2 consecutive).
898    fn cleanup_blank_lines(&mut self) {
899        let mut result = String::new();
900        let mut blank_count = 0;
901
902        for line in self.content.lines() {
903            if line.trim().is_empty() {
904                blank_count += 1;
905                if blank_count <= 2 {
906                    result.push_str(line);
907                    result.push('\n');
908                }
909            } else {
910                blank_count = 0;
911                result.push_str(line);
912                result.push('\n');
913            }
914        }
915
916        // Preserve trailing content (file may not end with newline)
917        if !self.content.ends_with('\n') && result.ends_with('\n') {
918            result.pop();
919        }
920
921        self.content = result;
922    }
923
924    /// Collect all cell function names from the file.
925    fn collect_cell_names(&self, file: &SynFile) -> HashSet<String> {
926        let mut names = HashSet::new();
927
928        for item in &file.items {
929            if let syn::Item::Fn(func) = item
930                && Self::has_cell_attribute(&func.attrs) {
931                    names.insert(func.sig.ident.to_string());
932                }
933        }
934
935        names
936    }
937
938    /// Collect all cells with their spans in source order.
939    /// Returns Vec of (name, start_line, end_line).
940    fn collect_cell_spans(&self, file: &SynFile) -> Vec<(String, usize, usize)> {
941        let mut cells = Vec::new();
942
943        for item in &file.items {
944            if let syn::Item::Fn(func) = item
945                && Self::has_cell_attribute(&func.attrs) {
946                    let name = func.sig.ident.to_string();
947
948                    // Start from the first attribute or doc comment
949                    let start_line = if !func.attrs.is_empty() {
950                        func.attrs
951                            .iter()
952                            .map(|a| a.span().start().line)
953                            .min()
954                            .unwrap_or(func.sig.fn_token.span.start().line)
955                    } else {
956                        func.sig.fn_token.span.start().line
957                    };
958
959                    let end_line = func.block.brace_token.span.close().end().line;
960
961                    cells.push((name, start_line, end_line));
962                }
963        }
964
965        cells
966    }
967
968    /// Generate a unique cell name (new_cell_1, new_cell_2, etc.).
969    fn generate_unique_name(&self, existing: &HashSet<String>) -> String {
970        for i in 1.. {
971            let name = format!("new_cell_{}", i);
972            if !existing.contains(&name) {
973                return name;
974            }
975        }
976        unreachable!()
977    }
978
979    /// Generate a unique copy name (e.g., `cell_copy`, `cell_copy_2`).
980    fn generate_copy_name(&self, original: &str, existing: &HashSet<String>) -> String {
981        // Try `original_copy` first
982        let base_copy = format!("{}_copy", original);
983        if !existing.contains(&base_copy) {
984            return base_copy;
985        }
986
987        // Then try `original_copy_2`, `original_copy_3`, etc.
988        for i in 2.. {
989            let name = format!("{}_copy_{}", original, i);
990            if !existing.contains(&name) {
991                return name;
992            }
993        }
994        unreachable!()
995    }
996
997    /// Find the byte position where the new cell should be inserted.
998    fn find_insert_position(&self, file: &SynFile, after_cell_id: Option<&str>) -> Result<usize> {
999        let lines: Vec<&str> = self.content.lines().collect();
1000
1001        // Track the end position of cells
1002        let mut last_cell_end_line = 0;
1003        let mut target_end_line = None;
1004
1005        for item in &file.items {
1006            if let syn::Item::Fn(func) = item
1007                && Self::has_cell_attribute(&func.attrs) {
1008                    let name = func.sig.ident.to_string();
1009
1010                    // Get the end line of this function
1011                    let end_line = func.block.brace_token.span.close().end().line;
1012
1013                    if let Some(target) = after_cell_id
1014                        && name == target {
1015                            target_end_line = Some(end_line);
1016                            break;
1017                        }
1018
1019                    last_cell_end_line = end_line;
1020                }
1021        }
1022
1023        // Determine which line to insert after
1024        let insert_after_line = match after_cell_id {
1025            Some(id) => target_end_line.ok_or_else(|| {
1026                Error::CellNotFound(format!("Cell '{}' not found", id))
1027            })?,
1028            None => {
1029                // Insert at end - if no cells, insert at end of file
1030                if last_cell_end_line == 0 {
1031                    return Ok(self.content.len());
1032                }
1033                last_cell_end_line
1034            }
1035        };
1036
1037        // Convert line number to byte offset (lines are 1-indexed from syn)
1038        let byte_offset = self.line_to_byte_offset(insert_after_line, &lines);
1039
1040        Ok(byte_offset)
1041    }
1042
1043    /// Convert a 1-indexed line number to a byte offset (end of that line).
1044    fn line_to_byte_offset(&self, line: usize, lines: &[&str]) -> usize {
1045        if line == 0 || line > lines.len() {
1046            return self.content.len();
1047        }
1048
1049        // Sum the bytes of all lines up to and including the target line
1050        let mut offset = 0;
1051        for (i, l) in lines.iter().enumerate() {
1052            offset += l.len();
1053            offset += 1; // newline character
1054
1055            if i + 1 == line {
1056                break;
1057            }
1058        }
1059
1060        offset.min(self.content.len())
1061    }
1062
1063    /// Generate the code for a new cell.
1064    fn generate_cell_code(&self, name: &str) -> String {
1065        format!(
1066            r#"
1067
1068/// New cell
1069#[venus::cell]
1070pub fn {}() -> String {{
1071    "Hello".to_string()
1072}}
1073"#,
1074            name
1075        )
1076    }
1077
1078    /// Check if a function has the #[venus::cell] attribute.
1079    fn has_cell_attribute(attrs: &[Attribute]) -> bool {
1080        attrs.iter().any(|attr| {
1081            let path = attr.path();
1082            let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
1083
1084            // Match #[venus::cell] or #[cell] (if imported)
1085            (segments.len() == 2 && segments[0] == "venus" && segments[1] == "cell")
1086                || (segments.len() == 1 && segments[0] == "cell")
1087        })
1088    }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093    use super::*;
1094    use std::io::Write;
1095    use tempfile::NamedTempFile;
1096
1097    fn create_temp_file(content: &str) -> NamedTempFile {
1098        let mut file = NamedTempFile::new().unwrap();
1099        file.write_all(content.as_bytes()).unwrap();
1100        file
1101    }
1102
1103    #[test]
1104    fn test_insert_cell_at_end() {
1105        let source = r#"use venus::prelude::*;
1106
1107/// First cell
1108#[venus::cell]
1109pub fn first() -> i32 {
1110    1
1111}
1112"#;
1113
1114        let file = create_temp_file(source);
1115        let mut editor = SourceEditor::load(file.path()).unwrap();
1116
1117        let name = editor.insert_cell(None).unwrap();
1118        assert_eq!(name, "new_cell_1");
1119
1120        // Check that the new cell is in the content
1121        assert!(editor.content.contains("#[venus::cell]"));
1122        assert!(editor.content.contains("pub fn new_cell_1()"));
1123    }
1124
1125    #[test]
1126    fn test_insert_cell_after_specific() {
1127        let source = r#"use venus::prelude::*;
1128
1129/// First cell
1130#[venus::cell]
1131pub fn first() -> i32 {
1132    1
1133}
1134
1135/// Second cell
1136#[venus::cell]
1137pub fn second(first: &i32) -> i32 {
1138    *first + 1
1139}
1140"#;
1141
1142        let file = create_temp_file(source);
1143        let mut editor = SourceEditor::load(file.path()).unwrap();
1144
1145        let name = editor.insert_cell(Some("first")).unwrap();
1146        assert_eq!(name, "new_cell_1");
1147
1148        // Verify the new cell appears after 'first' but before 'second'
1149        let first_pos = editor.content.find("pub fn first()").unwrap();
1150        let new_pos = editor.content.find("pub fn new_cell_1()").unwrap();
1151        let second_pos = editor.content.find("pub fn second(").unwrap();
1152
1153        assert!(first_pos < new_pos);
1154        assert!(new_pos < second_pos);
1155    }
1156
1157    #[test]
1158    fn test_unique_name_generation() {
1159        let source = r#"use venus::prelude::*;
1160
1161#[venus::cell]
1162pub fn new_cell_1() -> i32 { 1 }
1163
1164#[venus::cell]
1165pub fn new_cell_2() -> i32 { 2 }
1166"#;
1167
1168        let file = create_temp_file(source);
1169        let mut editor = SourceEditor::load(file.path()).unwrap();
1170
1171        let name = editor.insert_cell(None).unwrap();
1172        assert_eq!(name, "new_cell_3");
1173    }
1174
1175    #[test]
1176    fn test_insert_into_empty_file() {
1177        let source = r#"use venus::prelude::*;
1178
1179fn main() {}
1180"#;
1181
1182        let file = create_temp_file(source);
1183        let mut editor = SourceEditor::load(file.path()).unwrap();
1184
1185        let name = editor.insert_cell(None).unwrap();
1186        assert_eq!(name, "new_cell_1");
1187        assert!(editor.content.contains("pub fn new_cell_1()"));
1188    }
1189
1190    #[test]
1191    fn test_save() {
1192        let source = r#"#[venus::cell]
1193pub fn test() -> i32 { 1 }
1194"#;
1195
1196        let file = create_temp_file(source);
1197        let path = file.path().to_path_buf();
1198
1199        {
1200            let mut editor = SourceEditor::load(&path).unwrap();
1201            editor.insert_cell(None).unwrap();
1202            editor.save().unwrap();
1203        }
1204
1205        // Read back and verify
1206        let content = fs::read_to_string(&path).unwrap();
1207        assert!(content.contains("pub fn new_cell_1()"));
1208    }
1209
1210    #[test]
1211    fn test_delete_cell() {
1212        let source = r#"use venus::prelude::*;
1213
1214/// First cell
1215#[venus::cell]
1216pub fn first() -> i32 {
1217    1
1218}
1219
1220/// Second cell
1221#[venus::cell]
1222pub fn second() -> i32 {
1223    2
1224}
1225"#;
1226
1227        let file = create_temp_file(source);
1228        let mut editor = SourceEditor::load(file.path()).unwrap();
1229
1230        let name = editor.delete_cell("first").unwrap();
1231        assert_eq!(name, "first");
1232
1233        // Verify first cell is gone but second remains
1234        assert!(!editor.content.contains("pub fn first()"));
1235        assert!(editor.content.contains("pub fn second()"));
1236        // Header should remain
1237        assert!(editor.content.contains("use venus::prelude::*;"));
1238    }
1239
1240    #[test]
1241    fn test_delete_cell_with_doc_comments() {
1242        let source = r#"use venus::prelude::*;
1243
1244/// This is a doc comment
1245/// with multiple lines
1246#[venus::cell]
1247pub fn documented() -> i32 {
1248    42
1249}
1250
1251#[venus::cell]
1252pub fn other() -> i32 {
1253    1
1254}
1255"#;
1256
1257        let file = create_temp_file(source);
1258        let mut editor = SourceEditor::load(file.path()).unwrap();
1259
1260        editor.delete_cell("documented").unwrap();
1261
1262        // Verify the doc comments are also removed
1263        assert!(!editor.content.contains("This is a doc comment"));
1264        assert!(!editor.content.contains("pub fn documented()"));
1265        assert!(editor.content.contains("pub fn other()"));
1266    }
1267
1268    #[test]
1269    fn test_delete_nonexistent_cell() {
1270        let source = r#"#[venus::cell]
1271pub fn exists() -> i32 { 1 }
1272"#;
1273
1274        let file = create_temp_file(source);
1275        let mut editor = SourceEditor::load(file.path()).unwrap();
1276
1277        let result = editor.delete_cell("nonexistent");
1278        assert!(result.is_err());
1279    }
1280
1281    #[test]
1282    fn test_delete_last_cell() {
1283        let source = r#"use venus::prelude::*;
1284
1285/// Only cell
1286#[venus::cell]
1287pub fn only() -> i32 {
1288    1
1289}
1290"#;
1291
1292        let file = create_temp_file(source);
1293        let mut editor = SourceEditor::load(file.path()).unwrap();
1294
1295        editor.delete_cell("only").unwrap();
1296
1297        // Should still have the use statement
1298        assert!(editor.content.contains("use venus::prelude::*;"));
1299        assert!(!editor.content.contains("pub fn only()"));
1300    }
1301
1302    #[test]
1303    fn test_duplicate_cell() {
1304        let source = r#"use venus::prelude::*;
1305
1306/// First cell
1307#[venus::cell]
1308pub fn first() -> i32 {
1309    42
1310}
1311"#;
1312
1313        let file = create_temp_file(source);
1314        let mut editor = SourceEditor::load(file.path()).unwrap();
1315
1316        let name = editor.duplicate_cell("first").unwrap();
1317        assert_eq!(name, "first_copy");
1318
1319        // Both original and copy should exist
1320        assert!(editor.content.contains("pub fn first()"));
1321        assert!(editor.content.contains("pub fn first_copy()"));
1322        // Copy should have same body
1323        assert!(editor.content.matches("42").count() == 2);
1324    }
1325
1326    #[test]
1327    fn test_duplicate_cell_preserves_doc_comments() {
1328        let source = r#"use venus::prelude::*;
1329
1330/// This is a documented cell
1331/// with multiple lines of docs
1332#[venus::cell]
1333pub fn documented() -> String {
1334    "hello".to_string()
1335}
1336"#;
1337
1338        let file = create_temp_file(source);
1339        let mut editor = SourceEditor::load(file.path()).unwrap();
1340
1341        let name = editor.duplicate_cell("documented").unwrap();
1342        assert_eq!(name, "documented_copy");
1343
1344        // Doc comments should be duplicated
1345        assert_eq!(editor.content.matches("This is a documented cell").count(), 2);
1346        assert!(editor.content.contains("pub fn documented_copy()"));
1347    }
1348
1349    #[test]
1350    fn test_duplicate_cell_unique_naming() {
1351        let source = r#"use venus::prelude::*;
1352
1353#[venus::cell]
1354pub fn original() -> i32 { 1 }
1355
1356#[venus::cell]
1357pub fn original_copy() -> i32 { 2 }
1358"#;
1359
1360        let file = create_temp_file(source);
1361        let mut editor = SourceEditor::load(file.path()).unwrap();
1362
1363        let name = editor.duplicate_cell("original").unwrap();
1364        // Should be original_copy_2 since original_copy already exists
1365        assert_eq!(name, "original_copy_2");
1366        assert!(editor.content.contains("pub fn original_copy_2()"));
1367    }
1368
1369    #[test]
1370    fn test_duplicate_nonexistent_cell() {
1371        let source = r#"#[venus::cell]
1372pub fn exists() -> i32 { 1 }
1373"#;
1374
1375        let file = create_temp_file(source);
1376        let mut editor = SourceEditor::load(file.path()).unwrap();
1377
1378        let result = editor.duplicate_cell("nonexistent");
1379        assert!(result.is_err());
1380    }
1381
1382    #[test]
1383    fn test_move_cell_down() {
1384        let source = r#"use venus::prelude::*;
1385
1386/// First cell
1387#[venus::cell]
1388pub fn first() -> i32 {
1389    1
1390}
1391
1392/// Second cell
1393#[venus::cell]
1394pub fn second() -> i32 {
1395    2
1396}
1397"#;
1398
1399        let file = create_temp_file(source);
1400        let mut editor = SourceEditor::load(file.path()).unwrap();
1401
1402        editor.move_cell("first", MoveDirection::Down).unwrap();
1403
1404        // Second should now come before first
1405        let second_pos = editor.content.find("pub fn second()").unwrap();
1406        let first_pos = editor.content.find("pub fn first()").unwrap();
1407        assert!(second_pos < first_pos);
1408    }
1409
1410    #[test]
1411    fn test_move_cell_up() {
1412        let source = r#"use venus::prelude::*;
1413
1414/// First cell
1415#[venus::cell]
1416pub fn first() -> i32 {
1417    1
1418}
1419
1420/// Second cell
1421#[venus::cell]
1422pub fn second() -> i32 {
1423    2
1424}
1425"#;
1426
1427        let file = create_temp_file(source);
1428        let mut editor = SourceEditor::load(file.path()).unwrap();
1429
1430        editor.move_cell("second", MoveDirection::Up).unwrap();
1431
1432        // Second should now come before first
1433        let second_pos = editor.content.find("pub fn second()").unwrap();
1434        let first_pos = editor.content.find("pub fn first()").unwrap();
1435        assert!(second_pos < first_pos);
1436    }
1437
1438    #[test]
1439    fn test_move_first_cell_up_fails() {
1440        let source = r#"#[venus::cell]
1441pub fn first() -> i32 { 1 }
1442
1443#[venus::cell]
1444pub fn second() -> i32 { 2 }
1445"#;
1446
1447        let file = create_temp_file(source);
1448        let mut editor = SourceEditor::load(file.path()).unwrap();
1449
1450        let result = editor.move_cell("first", MoveDirection::Up);
1451        assert!(result.is_err());
1452    }
1453
1454    #[test]
1455    fn test_move_last_cell_down_fails() {
1456        let source = r#"#[venus::cell]
1457pub fn first() -> i32 { 1 }
1458
1459#[venus::cell]
1460pub fn second() -> i32 { 2 }
1461"#;
1462
1463        let file = create_temp_file(source);
1464        let mut editor = SourceEditor::load(file.path()).unwrap();
1465
1466        let result = editor.move_cell("second", MoveDirection::Down);
1467        assert!(result.is_err());
1468    }
1469
1470    #[test]
1471    fn test_move_preserves_doc_comments() {
1472        let source = r#"use venus::prelude::*;
1473
1474/// This is the first cell
1475/// with multiple lines
1476#[venus::cell]
1477pub fn first() -> i32 {
1478    1
1479}
1480
1481/// This is the second cell
1482#[venus::cell]
1483pub fn second() -> i32 {
1484    2
1485}
1486"#;
1487
1488        let file = create_temp_file(source);
1489        let mut editor = SourceEditor::load(file.path()).unwrap();
1490
1491        editor.move_cell("first", MoveDirection::Down).unwrap();
1492
1493        // Check doc comments are preserved and in right order
1494        let second_doc_pos = editor.content.find("This is the second cell").unwrap();
1495        let first_doc_pos = editor.content.find("This is the first cell").unwrap();
1496        assert!(second_doc_pos < first_doc_pos);
1497    }
1498}