ide_assists/handlers/
generate_constant.rs

1use crate::assist_context::{AssistContext, Assists};
2use hir::{HasVisibility, HirDisplay, Module};
3use ide_db::{
4    FileId,
5    assists::AssistId,
6    defs::{Definition, NameRefClass},
7};
8use syntax::{
9    AstNode, Direction, SyntaxKind, TextSize,
10    ast::{self, NameRef, edit::IndentLevel},
11};
12
13// Assist: generate_constant
14//
15// Generate a named constant.
16//
17// ```
18// struct S { i: usize }
19// impl S { pub fn new(n: usize) {} }
20// fn main() {
21//     let v = S::new(CAPA$0CITY);
22// }
23// ```
24// ->
25// ```
26// struct S { i: usize }
27// impl S { pub fn new(n: usize) {} }
28// fn main() {
29//     const CAPACITY: usize = $0;
30//     let v = S::new(CAPACITY);
31// }
32// ```
33
34pub(crate) fn generate_constant(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
35    let constant_token = ctx.find_node_at_offset::<ast::NameRef>()?;
36    if constant_token.to_string().chars().any(|it| !(it.is_uppercase() || it == '_')) {
37        cov_mark::hit!(not_constant_name);
38        return None;
39    }
40    if NameRefClass::classify(&ctx.sema, &constant_token).is_some() {
41        cov_mark::hit!(already_defined);
42        return None;
43    }
44    let expr = constant_token.syntax().ancestors().find_map(ast::Expr::cast)?;
45    let statement = expr.syntax().ancestors().find_map(ast::Stmt::cast)?;
46    let ty = ctx.sema.type_of_expr(&expr)?;
47    let scope = ctx.sema.scope(statement.syntax())?;
48    let constant_module = scope.module();
49    let type_name =
50        ty.original().display_source_code(ctx.db(), constant_module.into(), false).ok()?;
51    let target = statement.syntax().parent()?.text_range();
52    let path = constant_token.syntax().ancestors().find_map(ast::Path::cast)?;
53    if path.parent_path().is_some() {
54        cov_mark::hit!(not_last_path_segment);
55        return None;
56    }
57
58    let name_refs = path.segments().map(|s| s.name_ref());
59    let mut outer_exists = false;
60    let mut not_exist_name_ref = Vec::new();
61    let mut current_module = constant_module;
62    for name_ref in name_refs {
63        let name_ref_value = name_ref?;
64        let name_ref_class = NameRefClass::classify(&ctx.sema, &name_ref_value);
65        match name_ref_class {
66            Some(NameRefClass::Definition(Definition::Module(m), _)) => {
67                if !m.visibility(ctx.sema.db).is_visible_from(ctx.sema.db, constant_module.into()) {
68                    return None;
69                }
70                outer_exists = true;
71                current_module = m;
72            }
73            Some(_) => {
74                return None;
75            }
76            None => {
77                not_exist_name_ref.push(name_ref_value);
78            }
79        }
80    }
81    let (offset, indent, file_id, post_string) =
82        target_data_for_generate_constant(ctx, current_module, constant_module).unwrap_or_else(
83            || {
84                let indent = IndentLevel::from_node(statement.syntax());
85                (statement.syntax().text_range().start(), indent, None, format!("\n{indent}"))
86            },
87        );
88
89    let text = get_text_for_generate_constant(not_exist_name_ref, indent, outer_exists, type_name)?;
90    acc.add(AssistId::quick_fix("generate_constant"), "Generate constant", target, |builder| {
91        if let Some(file_id) = file_id {
92            builder.edit_file(file_id);
93        }
94        builder.insert(offset, format!("{text}{post_string}"));
95    })
96}
97
98fn get_text_for_generate_constant(
99    mut not_exist_name_ref: Vec<NameRef>,
100    indent: IndentLevel,
101    outer_exists: bool,
102    type_name: String,
103) -> Option<String> {
104    let constant_token = not_exist_name_ref.pop()?;
105    let vis = if not_exist_name_ref.is_empty() && !outer_exists { "" } else { "\npub " };
106    let mut text = format!("{vis}const {constant_token}: {type_name} = $0;");
107    while let Some(name_ref) = not_exist_name_ref.pop() {
108        let vis = if not_exist_name_ref.is_empty() && !outer_exists { "" } else { "\npub " };
109        text = text.replace('\n', "\n    ");
110        text = format!("{vis}mod {name_ref} {{{text}\n}}");
111    }
112    Some(text.replace('\n', &format!("\n{indent}")))
113}
114
115fn target_data_for_generate_constant(
116    ctx: &AssistContext<'_>,
117    current_module: Module,
118    constant_module: Module,
119) -> Option<(TextSize, IndentLevel, Option<FileId>, String)> {
120    if current_module == constant_module {
121        // insert in current file
122        return None;
123    }
124    let in_file_source = current_module.definition_source(ctx.sema.db);
125    let file_id = in_file_source.file_id.original_file(ctx.sema.db);
126    match in_file_source.value {
127        hir::ModuleSource::Module(module_node) => {
128            let indent = IndentLevel::from_node(module_node.syntax());
129            let l_curly_token = module_node.item_list()?.l_curly_token()?;
130            let offset = l_curly_token.text_range().end();
131
132            let siblings_has_newline = l_curly_token
133                .siblings_with_tokens(Direction::Next)
134                .any(|it| it.kind() == SyntaxKind::WHITESPACE && it.to_string().contains('\n'));
135            let post_string =
136                if siblings_has_newline { format!("{indent}") } else { format!("\n{indent}") };
137            Some((offset, indent + 1, Some(file_id.file_id(ctx.db())), post_string))
138        }
139        _ => Some((TextSize::from(0), 0.into(), Some(file_id.file_id(ctx.db())), "\n".into())),
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::tests::{check_assist, check_assist_not_applicable};
147
148    #[test]
149    fn test_trivial() {
150        check_assist(
151            generate_constant,
152            r#"struct S { i: usize }
153impl S {
154    pub fn new(n: usize) {}
155}
156fn main() {
157    let v = S::new(CAPA$0CITY);
158}"#,
159            r#"struct S { i: usize }
160impl S {
161    pub fn new(n: usize) {}
162}
163fn main() {
164    const CAPACITY: usize = $0;
165    let v = S::new(CAPACITY);
166}"#,
167        );
168    }
169    #[test]
170    fn test_wont_apply_when_defined() {
171        cov_mark::check!(already_defined);
172        check_assist_not_applicable(
173            generate_constant,
174            r#"struct S { i: usize }
175impl S {
176    pub fn new(n: usize) {}
177}
178fn main() {
179    const CAPACITY: usize = 10;
180    let v = S::new(CAPAC$0ITY);
181}"#,
182        );
183    }
184    #[test]
185    fn test_wont_apply_when_maybe_not_constant() {
186        cov_mark::check!(not_constant_name);
187        check_assist_not_applicable(
188            generate_constant,
189            r#"struct S { i: usize }
190impl S {
191    pub fn new(n: usize) {}
192}
193fn main() {
194    let v = S::new(capa$0city);
195}"#,
196        );
197    }
198
199    #[test]
200    fn test_constant_with_path() {
201        check_assist(
202            generate_constant,
203            r#"mod foo {}
204fn bar() -> i32 {
205    foo::A_CON$0STANT
206}"#,
207            r#"mod foo {
208    pub const A_CONSTANT: i32 = $0;
209}
210fn bar() -> i32 {
211    foo::A_CONSTANT
212}"#,
213        );
214    }
215
216    #[test]
217    fn test_constant_with_longer_path() {
218        check_assist(
219            generate_constant,
220            r#"mod foo {
221    pub mod goo {}
222}
223fn bar() -> i32 {
224    foo::goo::A_CON$0STANT
225}"#,
226            r#"mod foo {
227    pub mod goo {
228        pub const A_CONSTANT: i32 = $0;
229    }
230}
231fn bar() -> i32 {
232    foo::goo::A_CONSTANT
233}"#,
234        );
235    }
236
237    #[test]
238    fn test_constant_with_not_exist_longer_path() {
239        check_assist(
240            generate_constant,
241            r#"fn bar() -> i32 {
242    foo::goo::A_CON$0STANT
243}"#,
244            r#"mod foo {
245    pub mod goo {
246        pub const A_CONSTANT: i32 = $0;
247    }
248}
249fn bar() -> i32 {
250    foo::goo::A_CONSTANT
251}"#,
252        );
253    }
254
255    #[test]
256    fn test_wont_apply_when_not_last_path_segment() {
257        cov_mark::check!(not_last_path_segment);
258        check_assist_not_applicable(
259            generate_constant,
260            r#"mod foo {}
261fn bar() -> i32 {
262    foo::A_CON$0STANT::invalid_segment
263}"#,
264        );
265    }
266}