rem_extract/
extraction.rs

1use std::{
2    fs,
3    io::{
4        self,
5        ErrorKind
6    },
7    path::PathBuf
8};
9
10use ra_ap_ide_db::EditionedFileId;
11use ra_ap_project_model::{
12    CargoConfig,
13    ProjectWorkspace,
14    ProjectManifest,
15};
16
17use ra_ap_ide::{
18    Analysis,
19    AnalysisHost,
20    TextSize,
21};
22
23use ra_ap_syntax::{
24    algo, ast::HasName, AstNode, SourceFile
25};
26
27use ra_ap_hir::Semantics;
28
29use ra_ap_ide_assists::Assist;
30
31use ra_ap_vfs::AbsPathBuf;
32
33use crate::{
34    error::ExtractionError,
35    extraction_utils::{
36        apply_extract_function,
37        convert_to_abs_path_buf,
38        filter_extract_function_assist,
39        get_assists,
40        get_cargo_config,
41        get_cargo_toml,
42        get_manifest_dir,
43        load_project_manifest,
44        load_project_workspace,
45        load_workspace_data,
46        run_analysis,
47        check_braces,
48        check_comment,
49        trim_range,
50        generate_frange,
51    },
52};
53
54#[derive(Debug, PartialEq, Clone)]
55pub struct ExtractionInput {
56    pub file_path: String,
57    pub new_fn_name: String,
58    pub start_idx: u32,
59    pub end_idx: u32,
60}
61
62impl ExtractionInput {
63    pub fn new(
64        file_path: &str,
65        new_fn_name: &str,
66        start_idx: u32,
67        end_idx: u32,
68    ) -> Self { ExtractionInput {
69            file_path: file_path.to_string(),
70            new_fn_name: new_fn_name.to_string(),
71            start_idx,
72            end_idx,
73        }
74    }
75
76    #[allow(dead_code)]
77    pub fn new_absolute(
78        file_path: &str,
79        new_fn_name: &str,
80        start_idx: u32,
81        end_idx: u32,
82    ) -> Self { ExtractionInput {
83            file_path: convert_to_abs_path_buf(file_path).unwrap().as_str().to_string(),
84            new_fn_name: new_fn_name.to_string(),
85            start_idx,
86            end_idx,
87        }
88    }
89}
90
91// ========================================
92// Checks for the validity of the input
93// ========================================
94
95// Check if the file exists and is readable
96fn check_file_exists(file_path: &str) -> Result<(), ExtractionError> {
97    if fs::metadata(file_path).is_err() {
98        return Err(ExtractionError::Io(io::Error::new(
99            ErrorKind::NotFound,
100            format!("File not found: {}", file_path),
101        )));
102    }
103    Ok(())
104}
105
106// Check if the idx pair is valid
107fn check_idx(input: &ExtractionInput) -> Result<(), ExtractionError> {
108    if input.start_idx == input.end_idx {
109        return Err(ExtractionError::SameIdx);
110    } else if input.start_idx > input.end_idx {
111        return Err(ExtractionError::InvalidIdxPair);
112    }
113    if input.start_idx == 0 {
114        return Err(ExtractionError::InvalidStartIdx);
115    }
116    if input.end_idx == 0 {
117        return Err(ExtractionError::InvalidEndIdx);
118    }
119    Ok(())
120}
121
122fn verify_input(input: &ExtractionInput) -> Result<(), ExtractionError> {
123    // Execute each input validation step one by one
124    check_file_exists(&input.file_path)?;
125    check_idx(input)?;
126
127    Ok(())
128}
129
130// ========================================
131// Performs the method extraction
132// ========================================
133
134/// Function to extract the code segment based on cursor positions
135/// If successful, returns the `String` of the output code, followed by a
136/// `String` of the caller method
137pub fn extract_method(input: ExtractionInput) -> Result<(String, String), ExtractionError> {
138
139    // Extract the struct information
140    let input_path: &str = &input.file_path;
141    let callee_name: &str = &input.new_fn_name;
142    let start_idx: u32 = input.start_idx;
143    let end_idx: u32 = input.end_idx;
144
145    // Convert the input and output path to an `AbsPathBuf`
146    let input_abs_path: AbsPathBuf = convert_to_abs_path_buf(input_path).unwrap();
147
148    // Verify the input data
149    verify_input(&input)?;
150
151    let manifest_dir: PathBuf = get_manifest_dir(
152        &PathBuf::from(input_abs_path.as_str())
153    )?;
154    let cargo_toml: AbsPathBuf = get_cargo_toml( &manifest_dir );
155    // println!("Cargo.toml {:?}", cargo_toml);
156
157    let project_manifest: ProjectManifest = load_project_manifest( &cargo_toml );
158    // println!("Project Manifest {:?}", project_manifest);
159
160    let cargo_config: CargoConfig = get_cargo_config( &project_manifest );
161    // println!("Cargo Config {:?}", cargo_config);
162
163    let workspace: ProjectWorkspace = load_project_workspace( &project_manifest, &cargo_config );
164    // println!("Project Workspace {:?}", workspace);
165
166    let (db, vfs) = load_workspace_data(workspace, &cargo_config);
167
168    // Parse the cursor positions into the range
169    let range_: (u32, u32) = (
170        start_idx,
171        end_idx,
172    );
173
174    // Before we go too far, lets do few more quick checks now that we have the
175    // analysis
176    // 1. Check if the function to extract is not just a comment
177    // 2. Check if the function to extract has matching braces
178    // 3. Convert the range to a trimmed range.
179    let sema: Semantics<'_, ra_ap_ide::RootDatabase> = Semantics::new( &db );
180    let frange_: ra_ap_hir::FileRangeWrapper<ra_ap_vfs::FileId> = generate_frange( &input_abs_path, &vfs, range_.clone() );
181    let edition: EditionedFileId = EditionedFileId::current_edition( frange_.file_id );
182    let source_file: SourceFile = sema.parse( edition );
183    let range: (u32, u32) = trim_range( &source_file, &range_ );
184    check_comment( &source_file, &range )?;
185    check_braces( &source_file, &range )?;
186
187    let analysis_host: AnalysisHost = AnalysisHost::with_database( db );
188    let analysis: Analysis = run_analysis( analysis_host );
189
190    let assists: Vec<Assist> = get_assists( &analysis, &vfs, &input_abs_path, range );
191    let assist: Assist = filter_extract_function_assist( assists )?;
192
193
194    let modified_code: String = apply_extract_function(
195        &assist,
196        &input_abs_path,
197        &vfs,
198        &callee_name,
199    )?;
200
201    let parent_method: String = parent_method(
202        &source_file,
203        range,
204    )?;
205
206    Ok( (modified_code, parent_method) )
207}
208
209/// Gets the caller method, based on the input code and the cursor positions
210/// If successful, returns the `String` of the caller method
211/// If unsuccessful, returns an `ExtractionError`
212pub fn parent_method(
213    source_file: &SourceFile,
214    range: (u32, u32),
215) -> Result<String, ExtractionError> {
216    let start: TextSize = TextSize::new(range.0);
217
218    // We want the last function that occurs before the start of the range
219    let node: Option<ra_ap_syntax::ast::Fn> = algo::find_node_at_offset::<ra_ap_syntax::ast::Fn>(
220        source_file.syntax(),
221        start,
222    );
223
224    let fn_name: String = match node {
225        Some(n) => n.name().map_or("".to_string(), |name| name.text().to_string()),
226        None => "".to_string(),
227    };
228
229    if fn_name.is_empty() {
230        return Err(ExtractionError::ParentMethodNotFound);
231    }
232
233    Ok( fn_name.trim().to_string() )
234
235}