1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum MoveDirection {
22 Up,
24 Down,
26}
27
28pub struct SourceEditor {
33 path: PathBuf,
35 content: String,
37 _lock_file: Option<File>,
39}
40
41impl SourceEditor {
42 pub fn load(path: &Path) -> Result<Self> {
47 let lock_file = File::open(path)?;
49
50 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 pub fn insert_cell(&mut self, after_cell_id: Option<&str>) -> Result<String> {
72 let file: SynFile = syn::parse_str(&self.content)
74 .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
75
76 let existing_names = self.collect_cell_names(&file);
78
79 let new_name = self.generate_unique_name(&existing_names);
81
82 let insert_pos = self.find_insert_position(&file, after_cell_id)?;
84
85 let cell_code = self.generate_cell_code(&new_name);
87
88 self.content.insert_str(insert_pos, &cell_code);
90
91 Ok(new_name)
92 }
93
94 pub fn delete_cell(&mut self, cell_name: &str) -> Result<String> {
98 let file: SynFile = syn::parse_str(&self.content)
100 .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
101
102 let (start_line, end_line) = self.find_cell_span(&file, cell_name)?;
104
105 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 self.content = format!(
112 "{}{}",
113 &self.content[..start_offset],
114 &self.content[end_offset..]
115 );
116
117 self.cleanup_blank_lines();
119
120 Ok(cell_name.to_string())
121 }
122
123 pub fn duplicate_cell(&mut self, cell_name: &str) -> Result<String> {
129 let file: SynFile = syn::parse_str(&self.content)
131 .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
132
133 let (start_line, end_line) = self.find_cell_span(&file, cell_name)?;
135
136 let existing_names = self.collect_cell_names(&file);
138
139 let new_name = self.generate_copy_name(cell_name, &existing_names);
141
142 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 let new_cell_source = cell_source.replace(
150 &format!("fn {}(", cell_name),
151 &format!("fn {}(", new_name),
152 );
153
154 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 pub fn move_cell(&mut self, cell_name: &str, direction: MoveDirection) -> Result<()> {
165 let file: SynFile = syn::parse_str(&self.content)
167 .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
168
169 let cells = self.collect_cell_spans(&file);
171
172 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 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 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 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 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 pub fn rename_cell(&mut self, cell_name: &str, new_display_name: &str) -> Result<()> {
231 let file: SynFile = syn::parse_str(&self.content)
233 .map_err(|e| Error::Parse(format!("Failed to parse source: {}", e)))?;
234
235 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 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 if trimmed.starts_with('#') {
257 continue;
258 }
259
260 doc_lines.push(line);
261 }
262 }
263
264 let mut new_doc_lines = vec![format!("# {}", new_display_name)];
266 if !doc_lines.is_empty() {
267 new_doc_lines.push(String::new());
269 new_doc_lines.extend(doc_lines);
270 }
271
272 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 let fn_start_line = func.sig.fn_token.span.start().line;
286
287 let lines: Vec<&str> = self.content.lines().collect();
289
290 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 let new_doc_comment = new_doc_lines
299 .iter()
300 .map(|line| format!("{}/// {}", indent, line))
301 .collect::<Vec<_>>()
302 .join("\n");
303
304 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 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 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 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 let markdown_block = content
348 .lines()
349 .map(|line| format!("// {}", line))
350 .collect::<Vec<_>>()
351 .join("\n");
352
353 let insert_offset = if let Some(line_num) = after_line {
355 if line_num > lines.len() {
356 self.content.len()
357 } else {
358 self.find_block_end(line_num, &lines)
361 }
362 } else {
363 0 };
365
366 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 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 let insert_offset = if let Some(line_num) = after_line {
385 if line_num > lines.len() {
386 self.content.len()
387 } else {
388 self.find_block_end(line_num, &lines)
390 }
391 } else {
392 0 };
394
395 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 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 if line_num < start_line {
422 offset += line.len() + 1; continue;
424 }
425
426 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 found_opening && brace_depth == 0 {
438 offset += 1; return offset.min(self.content.len());
440 }
441 }
442 _ => {}
443 }
444 }
445
446 offset += 1; }
448
449 self.content.len()
451 }
452
453 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 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 let start_offset = self.line_start_offset(start_line, &lines);
477 let end_offset = self.line_to_byte_offset(end_line, &lines);
478
479 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 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 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 let start_offset = self.line_start_offset(start_line, &lines);
518 let end_offset = self.line_to_byte_offset(end_line, &lines);
519
520 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 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 let start_offset = self.line_start_offset(start_line, &lines);
553 let end_offset = self.line_to_byte_offset(end_line, &lines);
554
555 self.content = format!(
557 "{}{}",
558 &self.content[..start_offset],
559 &self.content[end_offset..]
560 );
561
562 self.cleanup_blank_lines();
564
565 Ok(())
566 }
567
568 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 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 let (swap_start_line, swap_end_line) = match direction {
593 MoveDirection::Up => {
594 if start_line == 1 {
596 return Err(Error::InvalidOperation("Cannot move first block up".to_string()));
597 }
598
599 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 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 if end_line >= lines.len() {
620 return Err(Error::InvalidOperation("Cannot move last block down".to_string()));
621 }
622
623 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 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 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 match direction {
651 MoveDirection::Up => {
652 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 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 pub fn save(&self) -> Result<()> {
683 fs::write(&self.path, &self.content)?;
684 Ok(())
686 }
687
688 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 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 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 self.find_insert_position(&file, Some(after_name))?
737 } else {
738 let cells = self.collect_cell_spans(&file);
740 if cells.is_empty() {
741 self.content.len()
743 } else {
744 let lines: Vec<&str> = self.content.lines().collect();
746 self.line_start_offset(cells[0].1, &lines)
747 }
748 };
749
750 let insert_code = if after_cell_name.is_some() {
752 format!("\n\n{}", source.trim())
753 } else {
754 format!("{}\n\n", source.trim())
756 };
757
758 self.content.insert_str(insert_pos, &insert_code);
759
760 Ok(())
761 }
762
763 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 let start_line = if !func.attrs.is_empty() {
773 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 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 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 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 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 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 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 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; }
882
883 offset.min(self.content.len())
884 }
885
886 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 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 if !self.content.ends_with('\n') && result.ends_with('\n') {
918 result.pop();
919 }
920
921 self.content = result;
922 }
923
924 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 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 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 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 fn generate_copy_name(&self, original: &str, existing: &HashSet<String>) -> String {
981 let base_copy = format!("{}_copy", original);
983 if !existing.contains(&base_copy) {
984 return base_copy;
985 }
986
987 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 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 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 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 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 if last_cell_end_line == 0 {
1031 return Ok(self.content.len());
1032 }
1033 last_cell_end_line
1034 }
1035 };
1036
1037 let byte_offset = self.line_to_byte_offset(insert_after_line, &lines);
1039
1040 Ok(byte_offset)
1041 }
1042
1043 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 let mut offset = 0;
1051 for (i, l) in lines.iter().enumerate() {
1052 offset += l.len();
1053 offset += 1; if i + 1 == line {
1056 break;
1057 }
1058 }
1059
1060 offset.min(self.content.len())
1061 }
1062
1063 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 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 (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 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 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 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 assert!(!editor.content.contains("pub fn first()"));
1235 assert!(editor.content.contains("pub fn second()"));
1236 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 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 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 assert!(editor.content.contains("pub fn first()"));
1321 assert!(editor.content.contains("pub fn first_copy()"));
1322 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 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 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 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 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 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}