1use crate::error::ExtractionError;
5
6use std::{
7 env,
8 fs,
9 path::PathBuf,
10 io::ErrorKind,
11};
12
13use camino::Utf8PathBuf;
14
15use ra_ap_project_model::{
16 CargoConfig,
17 ProjectWorkspace,
18 ProjectManifest,
19};
20
21use ra_ap_ide::{
22 Analysis,
23 AnalysisHost,
24 DiagnosticsConfig,
25 FileRange,
26 RootDatabase,
27 SingleResolve,
28 TextRange,
29 TextSize,
30 TextEdit,
31 SnippetEdit,
32 SourceChange,
33};
34
35use ra_ap_ide_db::{
36 imports::insert_use::{
37 ImportGranularity,
38 InsertUseConfig,
39 PrefixKind,
40 },
41 SnippetCap
42};
43
44use ra_ap_ide_assists::{
45 Assist,
46 AssistConfig,
47 AssistKind,
48 AssistResolveStrategy,
49};
50
51use ra_ap_vfs::{
52 AbsPathBuf,
53 VfsPath,
54 Vfs,
55 FileId,
56};
57
58use ra_ap_load_cargo::{
59 LoadCargoConfig,
60 ProcMacroServerChoice,
61 load_workspace,
62};
63
64use ra_ap_parser::{
65 T,
66 SyntaxKind::COMMENT,
67};
68
69use ra_ap_syntax::{
70 algo,
71 AstNode,
72 SourceFile
73};
74
75
76pub fn get_manifest_dir( path: &PathBuf ) -> Result<PathBuf, ExtractionError> {
90 let mut current_dir = if path.is_file() {
92 path.parent().unwrap_or(path)
93 } else {
94 path.as_path()
95 };
96
97 if fs::metadata(current_dir.join("Cargo.toml")).is_ok() {
99 return Ok(current_dir.to_path_buf());
100 }
101
102 while let Some(parent) = current_dir.parent() {
104 if fs::metadata(current_dir.join("Cargo.toml")).is_ok() {
105 return Ok(current_dir.to_path_buf());
106 }
107 current_dir = parent;
108 }
109
110 Err(ExtractionError::InvalidManifest)
112}
113
114pub fn convert_to_abs_path_buf(path: &str) -> Result<AbsPathBuf, Utf8PathBuf> {
120 if path.is_empty() {
121 return Err(Utf8PathBuf::from_path_buf(PathBuf::new()).unwrap());
122 }
123
124 if !path.is_ascii() {
126 return Err(Utf8PathBuf::from_path_buf(PathBuf::new()).unwrap());
127 }
128
129 match AbsPathBuf::try_from(path) {
131 Ok(abs_path_buf) => Ok(abs_path_buf),
132 Err(_) => {
133 let current_dir = env::current_dir().expect("Failed to get current directory");
135 let utf8_current_dir = Utf8PathBuf::from_path_buf(current_dir)
136 .expect("Failed to convert current directory to Utf8PathBuf");
137
138 let resolved_path = utf8_current_dir.join(path);
141
142 let normalized_path = resolved_path.canonicalize().unwrap_or(resolved_path.clone().into());
144
145 if let Some(parent) = normalized_path.parent() {
147 fs::create_dir_all(parent).expect("Failed to create directories");
148 }
149
150 let abs_path = AbsPathBuf::try_from(normalized_path.to_str().unwrap())
152 .map_err(|e| e); let abs_path_str: String = abs_path.unwrap().to_string();
159 let abs_path_str: String = abs_path_str
160 .replace(r"\\?\", "");
161
162 let new_abs_path = AbsPathBuf::try_from(abs_path_str.as_str())
163 .map_err(|e| e);
164
165 new_abs_path
167 }
168 }
169}
170
171pub fn get_cargo_toml( manifest_dir: &PathBuf ) -> AbsPathBuf {
174 AbsPathBuf::try_from(
175 manifest_dir
176 .join( "Cargo.toml" )
177 .to_str()
178 .unwrap()
179 ).unwrap()
180}
181
182pub fn load_project_manifest( cargo_toml: &AbsPathBuf ) -> ProjectManifest {
184 ProjectManifest::from_manifest_file(
185 cargo_toml.clone()
186 ).unwrap()
187}
188
189pub fn get_cargo_config( _manifest: &ProjectManifest ) -> CargoConfig {
192 CargoConfig::default()
193}
194
195pub fn progress( _message: String ) -> () {
196 }
198
199pub fn load_project_workspace(
201 project_manifest: &ProjectManifest,
202 cargo_config: &CargoConfig,
203) -> ProjectWorkspace {
204 ProjectWorkspace::load(
205 project_manifest.clone(),
206 cargo_config,
207 &progress
208 ).unwrap()
209}
210
211pub fn load_workspace_data(
213 workspace: ProjectWorkspace,
214 cargo_config: &CargoConfig,
215) -> (
216 RootDatabase,
217 Vfs
218) {
219 let load_cargo_config: LoadCargoConfig = LoadCargoConfig {
220 load_out_dirs_from_check: true,
221 with_proc_macro_server: ProcMacroServerChoice::None,
222 prefill_caches: false,
223 };
224
225 let (db,
226 vfs,
227 _proc_macro
228 ) = load_workspace(
229 workspace,
230 &cargo_config.extra_env,
231 &load_cargo_config
232 ).unwrap();
233
234 (db, vfs)
235}
236
237pub fn run_analysis( host: AnalysisHost ) -> Analysis {
239
240 let analysis: Analysis = host.analysis();
241
242 analysis
243}
244
245pub fn get_assists (
259 db: &RootDatabase,
261 vfs: &Vfs,
262 input_path: &AbsPathBuf,
263 range: (u32, u32), ) -> Vec<Assist> {
265 let assist_config: AssistConfig = generate_assist_config();
266 let resolve: AssistResolveStrategy = generate_resolve_strategy();
268 let frange: FileRange = generate_frange(input_path, vfs, range);
269
270 let assists: Vec<Assist> = ra_ap_ide_assists::assists(db, &assist_config, resolve, frange);
279
280 assists
281}
282
283pub fn generate_assist_config() -> AssistConfig {
285 let snippet_cap_: Option<SnippetCap> = None;
286 let allowed_assists: Vec<AssistKind> = vec![
287 AssistKind::RefactorExtract,
293 ];
294
295 let insert_use_: InsertUseConfig = InsertUseConfig {
296 granularity: ImportGranularity::Preserve,
297 enforce_granularity: false,
298 prefix_kind: PrefixKind::ByCrate,
299 group: false,
300 skip_glob_imports: false,
301 };
302
303 let assist_config: AssistConfig = AssistConfig {
304 snippet_cap: snippet_cap_,
305 allowed: Some(allowed_assists),
306 insert_use: insert_use_,
307 prefer_no_std: false,
308 prefer_prelude: false,
309 prefer_absolute: false,
310 assist_emit_must_use: false,
311 term_search_fuel: 2048, term_search_borrowck: false,
313 };
314 assist_config
315}
316
317pub fn generate_diagnostics_config() -> DiagnosticsConfig {
319 DiagnosticsConfig::test_sample()
320}
321
322pub fn generate_resolve_strategy() -> AssistResolveStrategy {
324 let single_resolve: SingleResolve = SingleResolve {
326 assist_id: "extract_function".to_string(),
327 assist_kind: AssistKind::RefactorExtract,
328 };
329
330 let resolve_strategy: AssistResolveStrategy = AssistResolveStrategy::Single(single_resolve);
331 resolve_strategy
332}
333
334pub fn generate_frange(
336 input_path: &AbsPathBuf,
337 vfs: &Vfs,
338 range: (u32, u32)
339) -> FileRange {
340 let vfs_path: VfsPath = VfsPath::new_real_path(
341 input_path
342 .as_str()
343 .to_string(),
344 );
345
346 let file_id_: FileId = vfs.file_id( &vfs_path ).unwrap();
347 let range_: TextRange = TextRange::new(
348 TextSize::try_from( range.0 ).unwrap(),
349 TextSize::try_from( range.1 ).unwrap(),
350 );
351
352 let frange: FileRange = FileRange {
353 file_id: file_id_,
354 range: range_,
355 };
356 frange
357}
358
359pub fn generate_frange_from_fileid(
360 file_id: FileId,
361 range: (u32, u32)
362) -> FileRange {
363 let range_: TextRange = TextRange::new(
364 TextSize::try_from( range.0 ).unwrap(),
365 TextSize::try_from( range.1 ).unwrap(),
366 );
367
368 let frange: FileRange = FileRange {
369 file_id: file_id,
370 range: range_,
371 };
372 frange
373}
374
375pub fn filter_extract_function_assist( assists: Vec<Assist> ) -> Result<Assist, ExtractionError> {
380 if let Some(extract_assist) = assists
381 .iter()
382 .find(|assist| assist.label == "Extract into function")
383 {
384 Ok(extract_assist.clone())
386 } else {
387 Err(ExtractionError::NoExtractFunction( assists ))
389 }
390}
391
392pub fn apply_extract_function(
397 assist: &Assist,
398 input_path: &AbsPathBuf,
399 vfs: &Vfs,
400 callee_name: &str,
401) -> Result<String, ExtractionError> {
402
403 let vfs_in_path: VfsPath = VfsPath::new_real_path(
404 input_path
405 .as_str()
406 .to_string(),
407 );
408
409 let src_change: SourceChange = assist.source_change
411 .as_ref()
412 .unwrap()
413 .clone();
414
415 let in_file_id: FileId = vfs.file_id( &vfs_in_path ).unwrap();
416 let (text_edit, maybe_snippet_edit) = src_change.get_source_and_snippet_edit(
417 in_file_id
418 ).unwrap();
419
420 let src_path: PathBuf = vfs_to_pathbuf( &vfs_in_path );
422 let text: String = fs::read_to_string( &src_path ).unwrap();
423 let edited_text: String = apply_edits(
424 text,
425 text_edit.clone(),
426 maybe_snippet_edit.clone(),
427 );
428
429 let renamed_text: String = rename_function(
432 edited_text,
433 "fun_name",
434 callee_name,
435 );
436
437 let fixed_cf_text: String = fixup_controlflow( renamed_text );
439
440 Ok( fixed_cf_text )
441}
442
443pub fn apply_edits(
445 text: String,
446 text_edit: TextEdit,
447 maybe_snippet_edit: Option<SnippetEdit>,
448) -> String {
449 let mut text: String = text; text_edit.apply( &mut text );
451 match maybe_snippet_edit {
452 Some( snippet_edit ) => {
453 snippet_edit.apply( &mut text );
454 },
455 None => (),
456 }
457 text
458}
459
460pub fn rename_function(
462 text: String,
463 old_name: &str,
464 new_name: &str,
465) -> String {
466 let mut text = text; let old_name: String = old_name.to_string();
468 let new_name: String = new_name.to_string();
469 text = text.replace( &old_name, &new_name );
470 text
471}
472
473fn vfs_to_pathbuf( vfs_path: &VfsPath ) -> PathBuf {
475 let path_str = vfs_path.to_string();
476 PathBuf::from( path_str )
478}
479
480pub fn check_comment(
485 source_file: &SourceFile,
486 range: &(u32, u32)
487) -> Result<(), ExtractionError> {
488 let frange: TextRange = TextRange::new(
489 TextSize::new(range.0),
490 TextSize::new(range.1),
491 );
492 let node = source_file
493 .syntax()
494 .covering_element(frange);
495
496 if node.kind() == COMMENT {
497 return Err(ExtractionError::CommentNotApplicable);
498 }
499
500 Ok(())
501}
502
503pub fn check_braces(
512 source_file: &SourceFile,
513 range: &(u32, u32)
514) -> Result<(), ExtractionError> {
515 let frange: TextRange = TextRange::new(
516 TextSize::new(range.0),
517 TextSize::new(range.1),
518 );
519 let node = source_file
520 .syntax()
521 .covering_element(frange);
522
523 if matches!(node.kind(), T!['{'] | T!['}'] | T!['('] | T![')'] | T!['['] | T![']']) {
524 return Err(ExtractionError::BracesNotApplicable);
525 }
526
527 Ok(())
528
529}
530
531pub fn trim_range(
533 source_file: &SourceFile,
534 range: &(u32, u32)
535) -> (u32, u32) {
536 let start = TextSize::new(range.0);
537 let end = TextSize::new(range.1);
538 let left = source_file
539 .syntax()
540 .token_at_offset( start )
541 .right_biased()
542 .and_then(|t| algo::skip_whitespace_token(t, rowan::Direction::Next))
543 .map(|t| t.text_range().start().clamp(start, end));
544 let right = source_file
545 .syntax()
546 .token_at_offset( end )
547 .left_biased()
548 .and_then(|t| algo::skip_whitespace_token(t, rowan::Direction::Prev))
549 .map(|t| t.text_range().end().clamp(start, end));
550
551 let trimmed_range = match (left, right) {
552 (Some(left), Some(right)) if left <= right => TextRange::new(left, right),
553 _ => TextRange::new(start, end),
555 };
556
557 ( trimmed_range.start().into(), trimmed_range.end().into() )
558
559}
560
561pub fn fixup_controlflow( text: String, ) -> String {
565 let mut text: String = text; let controlflow_ref: &str = "ControlFlow::";
567 if text.contains( controlflow_ref ) {
568 text = format!("use std::ops::ControlFlow;\n\n{}", text);
569 }
570 text
571}
572
573pub fn check_idx(start: &u32, end: &u32) -> Result<(), ExtractionError> {
575 let start = *start;
576 let end = *end;
577 if start == end {
578 return Err(ExtractionError::SameIdx);
579 } else if start > end {
580 return Err(ExtractionError::InvalidIdxPair);
581 }
582 if end == 0 {
583 return Err(ExtractionError::InvalidEndIdx);
584 }
585 Ok(())
586}
587
588pub fn check_file_exists(file_path: &str) -> Result<(), ExtractionError> {
590 if fs::metadata(file_path).is_err() {
591 return Err(ExtractionError::Io(std::io::Error::new(
592 ErrorKind::NotFound,
593 format!("File not found: {}", file_path),
594 )));
595 }
596 Ok(())
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602 use std::fs::{self, File};
603 use std::io::Write;
604 use std::env;
605 use camino::Utf8Path;
606
607 fn setup_temp_project() -> PathBuf {
609 let temp_dir = env::temp_dir().join("test_project");
610 let _ = fs::create_dir_all(&temp_dir);
611 let cargo_toml = temp_dir.join("Cargo.toml");
612
613 let mut file = File::create(cargo_toml).unwrap();
614 writeln!(file, "[package]\nname = \"test_project\"\nversion = \"0.1.0\"").unwrap();
615
616 temp_dir
617 }
618
619 #[test]
621 fn test_get_manifest_dir_valid() {
622 let temp_dir = setup_temp_project();
623 let src_dir = temp_dir.join("src");
624 let _ = fs::create_dir_all(&src_dir);
625 let main_file = src_dir.join("main.rs");
626 File::create(&main_file).unwrap();
627
628 let result = get_manifest_dir(&main_file);
629 assert!(result.is_ok());
630 assert_eq!(result.unwrap(), temp_dir);
631 }
632
633 #[test]
635 fn test_get_manifest_dir_invalid_manifest() {
636 let temp_dir = env::temp_dir().join("test_invalid_project");
637 let _ = fs::create_dir_all(&temp_dir);
638 let src_dir = temp_dir.join("src");
639 let _ = fs::create_dir_all(&src_dir);
640 let main_file = src_dir.join("main.rs");
641 File::create(&main_file).unwrap();
642
643 let result = get_manifest_dir(&main_file);
644 assert!(result.is_err());
645
646 if let ExtractionError::InvalidManifest = result.unwrap_err() {
648 } else {
650 panic!("Expected InvalidManifest error");
651 }
652 }
653
654 #[test]
656 fn test_get_manifest_dir_directory() {
657 let temp_dir = setup_temp_project();
658 let src_dir = temp_dir.join("src");
659 let _ = fs::create_dir_all(&src_dir);
660
661 let result = get_manifest_dir(&src_dir);
662 assert!(result.is_ok());
663 assert_eq!(result.unwrap(), temp_dir);
664 }
665
666 #[test]
668 fn test_get_manifest_dir_non_existent_file() {
669 let temp_dir = env::temp_dir().join("test_non_existent_project");
670 let src_dir = temp_dir.join("src");
671
672 let non_existent_file = src_dir.join("does_not_exist.rs");
673 let result = get_manifest_dir(&non_existent_file);
674 assert!(result.is_err());
675
676 if let ExtractionError::InvalidManifest = result.unwrap_err() {
678 } else {
680 panic!("Expected InvalidManifest error");
681 }
682 }
683
684 #[test]
685 #[cfg(target_os = "windows")]
688 fn test_absolute_path_windows() {
689 let abs_path = r"C:\Windows\System32";
691 let result = convert_to_abs_path_buf(abs_path);
692 assert!(result.is_ok(), "Expected absolute path conversion to succeed");
693
694 let abs_path_buf = result.unwrap();
696 assert_eq!(<AbsPathBuf as AsRef<Utf8Path>>::as_ref(&abs_path_buf), Utf8Path::new(abs_path));
697 }
698
699 #[test]
700 #[cfg(target_os = "windows")]
703 fn test_relative_path_windows() {
704 let rel_path = r"src\main.rs";
706 let result = convert_to_abs_path_buf(rel_path);
707 assert!(result.is_ok(), "Expected relative path conversion to succeed");
708
709 let current_dir = env::current_dir().unwrap();
711 let expected_abs_path = Utf8PathBuf::from_path_buf(current_dir).unwrap().join(rel_path);
712 let abs_path_buf = result.unwrap();
713
714 let left_path = <AbsPathBuf as AsRef<Utf8Path>>::as_ref(&abs_path_buf).to_string().replace(r"\\?\", "");
716 let right_path = expected_abs_path.as_path().to_string().replace(r"\\?\", "");
717 assert_eq!(left_path, right_path);
718 }
719
720 #[test]
721 #[cfg(target_os = "windows")]
724 fn test_invalid_utf8_path_windows() {
725 let invalid_utf8_path = r"C:\invalid\�path";
727 let result = convert_to_abs_path_buf(invalid_utf8_path);
728 assert!(result.is_err(), "Expected invalid UTF-8 path to fail conversion");
729 }
730
731 #[test]
732 fn test_empty_path_windows() {
733 let empty_path = "";
735 let result = convert_to_abs_path_buf(empty_path);
736 assert!(result.is_err(), "Expected empty path to fail conversion");
737 }
738
739 #[test]
740 fn test_root_path_windows() {
741 let root_path = r"C:\";
743 let result = convert_to_abs_path_buf(root_path);
744 assert!(result.is_ok(), "Expected root path conversion to succeed");
745
746 let abs_path_buf = result.unwrap();
747 assert_eq!(<AbsPathBuf as AsRef<Utf8Path>>::as_ref(&abs_path_buf), Utf8Path::new(root_path));
748 }
749
750 #[test]
751 fn test_resolve_relative_path_windows() {
752 let complex_rel_path = r"src\..\Cargo.toml";
754 let result = convert_to_abs_path_buf(complex_rel_path);
755 assert!(result.is_ok(), "Expected complex relative path conversion to succeed");
756
757 let current_dir = env::current_dir().unwrap();
759 let expected_abs_path = Utf8PathBuf::from_path_buf(current_dir)
760 .unwrap()
761 .join(complex_rel_path)
762 .canonicalize_utf8()
763 .unwrap();
764 let abs_path_buf = result.unwrap();
765
766 let left_path = <AbsPathBuf as AsRef<Utf8Path>>::as_ref(&abs_path_buf).to_string().replace(r"\\?\", "");
768 let right_path = expected_abs_path.as_path().to_string().replace(r"\\?\", "");
769 assert_eq!( left_path, right_path );
770
771 }
772}