Skip to content

Commit 03b7efa

Browse files
committed
intial work extract-struct-from-function-signature
1 parent e57f184 commit 03b7efa

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
use hir::{Function, ModuleDef, Visibility};
2+
use ide_db::{RootDatabase, assists::AssistId, path_transform::PathTransform};
3+
use itertools::Itertools;
4+
use syntax::{
5+
ast::{
6+
self, edit::{AstNodeEdit, IndentLevel}, make, HasAttrs, HasGenericParams, HasName, HasVisibility
7+
}, match_ast, ted, AstNode, SyntaxElement, SyntaxKind, SyntaxNode, T
8+
};
9+
10+
use crate::{AssistContext, Assists};
11+
// Assist: extract_struct_from_function_signature
12+
//
13+
// Extracts a struct (part) of the signature of a function.
14+
//
15+
// ```
16+
// fn foo(bar: u32, baz: u32) { ... }
17+
// ```
18+
// ->
19+
// ```
20+
// struct FooStruct {
21+
// bar: u32,
22+
// baz: u32,
23+
// }
24+
//
25+
// fn foo(FooStruct) { ... }
26+
// ```
27+
28+
pub(crate) fn extract_struct_from_function_signature(
29+
acc: &mut Assists,
30+
ctx: &AssistContext<'_>,
31+
) -> Option<()> {
32+
// TODO: get more specific than param list
33+
// how to get function name and param list/part of param list the is selected seperatly
34+
// or maybe just auto generate random name not based on function name?
35+
let fn_ast = ctx.find_node_at_offset::<ast::Fn>()?;
36+
let fn_name = fn_ast.name()?;
37+
38+
let fn_hir = ctx.sema.to_def(&fn_ast)?;
39+
if existing_definition(ctx.db(), &fn_name, &fn_hir) {
40+
cov_mark::hit!(test_extract_function_signature_not_applicable_if_struct_exists);
41+
return None;
42+
}
43+
44+
// TODO: does this capture parenthesis
45+
let target = fn_ast.param_list()?.syntax().text_range();
46+
// TODO: special handiling for self?
47+
// TODO: special handling for destrutered types (or maybe just don't suppurt code action on
48+
// destructed types yet
49+
let field_list = make::record_field_list(
50+
fn_ast
51+
.param_list()?
52+
.params()
53+
.map(|param| Some(make::record_field(None, make::name("todo"), param.ty()?)))
54+
.collect::<Option<Vec<_>>>()?
55+
.into_iter(),
56+
);
57+
acc.add(
58+
AssistId::refactor_rewrite("extract_struct_from_function_signature"),
59+
"Extract struct from signature of a function",
60+
target,
61+
|builder| {
62+
builder.edit_file(ctx.vfs_file_id());
63+
64+
let generic_params = fn_ast
65+
.generic_param_list()
66+
.and_then(|known_generics| extract_generic_params(&known_generics, &field_list));
67+
let generics = generic_params.as_ref().map(|generics| generics.clone_for_update());
68+
69+
// resolve GenericArg in field_list to actual type
70+
let field_list = if let Some((target_scope, source_scope)) =
71+
ctx.sema.scope(fn_ast.syntax()).zip(ctx.sema.scope(field_list.syntax()))
72+
{
73+
let field_list = field_list.reset_indent();
74+
let field_list =
75+
PathTransform::generic_transformation(&target_scope, &source_scope)
76+
.apply(field_list.syntax());
77+
match_ast! {
78+
match field_list {
79+
ast::RecordFieldList(field_list) => field_list,
80+
_ => unreachable!(),
81+
}
82+
}
83+
} else {
84+
field_list.clone_for_update()
85+
};
86+
87+
let def =
88+
create_struct_def(fn_name.clone(), &fn_ast, &field_list, generics);
89+
90+
let indent = fn_ast.indent_level();
91+
let def = def.indent(indent);
92+
93+
ted::insert_all(
94+
ted::Position::before(fn_ast.syntax()),
95+
vec![
96+
def.syntax().clone().into(),
97+
make::tokens::whitespace(&format!("\n\n{indent}")).into(),
98+
],
99+
);
100+
},
101+
)
102+
}
103+
fn create_struct_def(
104+
name: ast::Name,
105+
fn_ast: &ast::Fn,
106+
field_list: &ast::RecordFieldList,
107+
generics: Option<ast::GenericParamList>,
108+
) -> ast::Struct {
109+
let enum_vis = fn_ast.visibility();
110+
111+
let insert_vis = |node: &'_ SyntaxNode, vis: &'_ SyntaxNode| {
112+
let vis = vis.clone_for_update();
113+
ted::insert(ted::Position::before(node), vis);
114+
};
115+
116+
// for fields without any existing visibility, use visibility of enum
117+
let field_list: ast::FieldList = {
118+
if let Some(vis) = &enum_vis {
119+
field_list
120+
.fields()
121+
.filter(|field| field.visibility().is_none())
122+
.filter_map(|field| field.name())
123+
.for_each(|it| insert_vis(it.syntax(), vis.syntax()));
124+
}
125+
126+
field_list.clone().into()
127+
};
128+
let field_list = field_list.indent(IndentLevel::single());
129+
130+
let strukt = make::struct_(enum_vis, name, generics, field_list).clone_for_update();
131+
132+
// take comments from variant
133+
ted::insert_all(
134+
ted::Position::first_child_of(strukt.syntax()),
135+
take_all_comments(fn_ast.syntax()),
136+
);
137+
138+
// copy attributes from enum
139+
ted::insert_all(
140+
ted::Position::first_child_of(strukt.syntax()),
141+
fn_ast
142+
.attrs()
143+
.flat_map(|it| {
144+
vec![it.syntax().clone_for_update().into(), make::tokens::single_newline().into()]
145+
})
146+
.collect(),
147+
);
148+
149+
strukt
150+
}
151+
// Note: this also detaches whitespace after comments,
152+
// since `SyntaxNode::splice_children` (and by extension `ted::insert_all_raw`)
153+
// detaches nodes. If we only took the comments, we'd leave behind the old whitespace.
154+
fn take_all_comments(node: &SyntaxNode) -> Vec<SyntaxElement> {
155+
let mut remove_next_ws = false;
156+
node.children_with_tokens()
157+
.filter_map(move |child| match child.kind() {
158+
SyntaxKind::COMMENT => {
159+
remove_next_ws = true;
160+
child.detach();
161+
Some(child)
162+
}
163+
SyntaxKind::WHITESPACE if remove_next_ws => {
164+
remove_next_ws = false;
165+
child.detach();
166+
Some(make::tokens::single_newline().into())
167+
}
168+
_ => {
169+
remove_next_ws = false;
170+
None
171+
}
172+
})
173+
.collect()
174+
}
175+
fn extract_generic_params(
176+
known_generics: &ast::GenericParamList,
177+
field_list: &ast::RecordFieldList,
178+
) -> Option<ast::GenericParamList> {
179+
let mut generics = known_generics.generic_params().map(|param| (param, false)).collect_vec();
180+
181+
let tagged_one = field_list
182+
.fields()
183+
.filter_map(|f| f.ty())
184+
.fold(false, |tagged, ty| tag_generics_in_function_signature(&ty, &mut generics) || tagged);
185+
186+
let generics = generics.into_iter().filter_map(|(param, tag)| tag.then_some(param));
187+
tagged_one.then(|| make::generic_param_list(generics))
188+
}
189+
fn tag_generics_in_function_signature(
190+
ty: &ast::Type,
191+
generics: &mut [(ast::GenericParam, bool)],
192+
) -> bool {
193+
let mut tagged_one = false;
194+
195+
for token in ty.syntax().descendants_with_tokens().filter_map(SyntaxElement::into_token) {
196+
for (param, tag) in generics.iter_mut().filter(|(_, tag)| !tag) {
197+
match param {
198+
ast::GenericParam::LifetimeParam(lt)
199+
if matches!(token.kind(), T![lifetime_ident]) =>
200+
{
201+
if let Some(lt) = lt.lifetime() {
202+
if lt.text().as_str() == token.text() {
203+
*tag = true;
204+
tagged_one = true;
205+
break;
206+
}
207+
}
208+
}
209+
param if matches!(token.kind(), T![ident]) => {
210+
if match param {
211+
ast::GenericParam::ConstParam(konst) => konst
212+
.name()
213+
.map(|name| name.text().as_str() == token.text())
214+
.unwrap_or_default(),
215+
ast::GenericParam::TypeParam(ty) => ty
216+
.name()
217+
.map(|name| name.text().as_str() == token.text())
218+
.unwrap_or_default(),
219+
ast::GenericParam::LifetimeParam(lt) => lt
220+
.lifetime()
221+
.map(|lt| lt.text().as_str() == token.text())
222+
.unwrap_or_default(),
223+
} {
224+
*tag = true;
225+
tagged_one = true;
226+
break;
227+
}
228+
}
229+
_ => (),
230+
}
231+
}
232+
}
233+
234+
tagged_one
235+
}
236+
fn existing_definition(db: &RootDatabase, variant_name: &ast::Name, variant: &Function) -> bool {
237+
variant
238+
.module(db)
239+
.scope(db, None)
240+
.into_iter()
241+
.filter(|(_, def)| match def {
242+
// only check type-namespace
243+
hir::ScopeDef::ModuleDef(def) => matches!(
244+
def,
245+
ModuleDef::Module(_)
246+
| ModuleDef::Adt(_)
247+
| ModuleDef::Variant(_)
248+
| ModuleDef::Trait(_)
249+
| ModuleDef::TypeAlias(_)
250+
| ModuleDef::BuiltinType(_)
251+
),
252+
_ => false,
253+
})
254+
.any(|(name, _)| name.as_str() == variant_name.text().trim_start_matches("r#"))
255+
}

crates/ide-assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ mod handlers {
233233
mod unwrap_type_to_generic_arg;
234234
mod wrap_return_type;
235235
mod wrap_unwrap_cfg_attr;
236+
mod extract_struct_from_function_signature;
236237

237238
pub(crate) fn all() -> &'static [Handler] {
238239
&[
@@ -280,6 +281,7 @@ mod handlers {
280281
expand_rest_pattern::expand_rest_pattern,
281282
extract_expressions_from_format_string::extract_expressions_from_format_string,
282283
extract_struct_from_enum_variant::extract_struct_from_enum_variant,
284+
extract_struct_from_function_signature::extract_struct_from_function_signature,
283285
extract_type_alias::extract_type_alias,
284286
fix_visibility::fix_visibility,
285287
flip_binexpr::flip_binexpr,

0 commit comments

Comments
 (0)