Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions crates/ide-assists/src/handlers/introduce_named_generic.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use syntax::{
ast::{self, edit_in_place::GenericParamsOwnerEdit, make, AstNode},
ast::{self, edit_in_place::GenericParamsOwnerEdit, make, AstNode, HasGenericParams},
ted,
};

Expand All @@ -14,7 +14,7 @@ use crate::{utils::suggest_name, AssistContext, AssistId, AssistKind, Assists};
// ```
// ->
// ```
// fn foo<B: Bar>(bar: B) {}
// fn foo<$0B: Bar>(bar: B) {}
// ```
pub(crate) fn introduce_named_generic(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let impl_trait_type = ctx.find_node_at_offset::<ast::ImplTraitType>()?;
Expand All @@ -39,7 +39,15 @@ pub(crate) fn introduce_named_generic(acc: &mut Assists, ctx: &AssistContext<'_>
let new_ty = make::ty(&type_param_name).clone_for_update();

ted::replace(impl_trait_type.syntax(), new_ty.syntax());
fn_.get_or_create_generic_param_list().add_generic_param(type_param.into())
fn_.get_or_create_generic_param_list().add_generic_param(type_param.into());

if let Some(cap) = ctx.config.snippet_cap {
if let Some(generic_param) =
fn_.generic_param_list().and_then(|it| it.generic_params().last())
{
edit.add_tabstop_before(cap, generic_param);
}
}
},
)
}
Expand All @@ -55,7 +63,7 @@ mod tests {
check_assist(
introduce_named_generic,
r#"fn foo<G>(bar: $0impl Bar) {}"#,
r#"fn foo<G, B: Bar>(bar: B) {}"#,
r#"fn foo<G, $0B: Bar>(bar: B) {}"#,
);
}

Expand All @@ -64,7 +72,7 @@ mod tests {
check_assist(
introduce_named_generic,
r#"fn foo(bar: $0impl Bar) {}"#,
r#"fn foo<B: Bar>(bar: B) {}"#,
r#"fn foo<$0B: Bar>(bar: B) {}"#,
);
}

Expand All @@ -73,7 +81,7 @@ mod tests {
check_assist(
introduce_named_generic,
r#"fn foo<G>(foo: impl Foo, bar: $0impl Bar) {}"#,
r#"fn foo<G, B: Bar>(foo: impl Foo, bar: B) {}"#,
r#"fn foo<G, $0B: Bar>(foo: impl Foo, bar: B) {}"#,
);
}

Expand All @@ -82,7 +90,7 @@ mod tests {
check_assist(
introduce_named_generic,
r#"fn foo<>(bar: $0impl Bar) {}"#,
r#"fn foo<B: Bar>(bar: B) {}"#,
r#"fn foo<$0B: Bar>(bar: B) {}"#,
);
}

Expand All @@ -95,7 +103,7 @@ fn foo<
>(bar: $0impl Bar) {}
"#,
r#"
fn foo<B: Bar
fn foo<$0B: Bar
>(bar: B) {}
"#,
);
Expand All @@ -108,7 +116,7 @@ fn foo<B: Bar
check_assist(
introduce_named_generic,
r#"fn foo<B>(bar: $0impl Bar) {}"#,
r#"fn foo<B, B: Bar>(bar: B) {}"#,
r#"fn foo<B, $0B: Bar>(bar: B) {}"#,
);
}

Expand All @@ -127,7 +135,7 @@ fn foo<
fn foo<
G: Foo,
F,
H, B: Bar,
H, $0B: Bar,
>(bar: B) {}
"#,
);
Expand All @@ -138,7 +146,7 @@ fn foo<
check_assist(
introduce_named_generic,
r#"fn foo(bar: $0impl Foo + Bar) {}"#,
r#"fn foo<F: Foo + Bar>(bar: F) {}"#,
r#"fn foo<$0F: Foo + Bar>(bar: F) {}"#,
);
}
}
2 changes: 1 addition & 1 deletion crates/ide-assists/src/tests/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1596,7 +1596,7 @@ fn doctest_introduce_named_generic() {
fn foo(bar: $0impl Bar) {}
"#####,
r#####"
fn foo<B: Bar>(bar: B) {}
fn foo<$0B: Bar>(bar: B) {}
"#####,
)
}
Expand Down
109 changes: 108 additions & 1 deletion crates/ide-db/src/source_change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::{collections::hash_map::Entry, iter, mem};

use base_db::{AnchoredPathBuf, FileId};
use stdx::{hash::NoHashHashMap, never};
use syntax::{algo, AstNode, SyntaxNode, SyntaxNodePtr, TextRange, TextSize};
use syntax::{algo, ast, ted, AstNode, SyntaxNode, SyntaxNodePtr, TextRange, TextSize};
use text_edit::{TextEdit, TextEditBuilder};

use crate::SnippetCap;
Expand Down Expand Up @@ -99,13 +99,21 @@ pub struct SourceChangeBuilder {

/// Maps the original, immutable `SyntaxNode` to a `clone_for_update` twin.
pub mutated_tree: Option<TreeMutator>,
/// Keeps track of where to place snippets
pub snippet_builder: Option<SnippetBuilder>,
}

pub struct TreeMutator {
immutable: SyntaxNode,
mutable_clone: SyntaxNode,
}

#[derive(Default)]
pub struct SnippetBuilder {
/// Where to place snippets at
places: Vec<PlaceSnippet>,
}

impl TreeMutator {
pub fn new(immutable: &SyntaxNode) -> TreeMutator {
let immutable = immutable.ancestors().last().unwrap();
Expand All @@ -131,6 +139,7 @@ impl SourceChangeBuilder {
source_change: SourceChange::default(),
trigger_signature_help: false,
mutated_tree: None,
snippet_builder: None,
}
}

Expand All @@ -140,6 +149,17 @@ impl SourceChangeBuilder {
}

fn commit(&mut self) {
// Render snippets first so that they get bundled into the tree diff
if let Some(mut snippets) = self.snippet_builder.take() {
// Last snippet always has stop index 0
let last_stop = snippets.places.pop().unwrap();
last_stop.place(0);

for (index, stop) in snippets.places.into_iter().enumerate() {
stop.place(index + 1)
}
}

if let Some(tm) = self.mutated_tree.take() {
algo::diff(&tm.immutable, &tm.mutable_clone).into_text_edit(&mut self.edit)
}
Expand Down Expand Up @@ -214,6 +234,30 @@ impl SourceChangeBuilder {
self.trigger_signature_help = true;
}

/// Adds a tabstop snippet to place the cursor before `node`
pub fn add_tabstop_before(&mut self, _cap: SnippetCap, node: impl AstNode) {
assert!(node.syntax().parent().is_some());
self.add_snippet(PlaceSnippet::Before(node.syntax().clone()));
}

/// Adds a tabstop snippet to place the cursor after `node`
pub fn add_tabstop_after(&mut self, _cap: SnippetCap, node: impl AstNode) {
assert!(node.syntax().parent().is_some());
self.add_snippet(PlaceSnippet::After(node.syntax().clone()));
}

/// Adds a snippet to move the cursor selected over `node`
pub fn add_placeholder_snippet(&mut self, _cap: SnippetCap, node: impl AstNode) {
assert!(node.syntax().parent().is_some());
self.add_snippet(PlaceSnippet::Over(node.syntax().clone()))
}

fn add_snippet(&mut self, snippet: PlaceSnippet) {
let snippet_builder = self.snippet_builder.get_or_insert(SnippetBuilder { places: vec![] });
snippet_builder.places.push(snippet);
self.source_change.is_snippet = true;
}

pub fn finish(mut self) -> SourceChange {
self.commit();
mem::take(&mut self.source_change)
Expand All @@ -236,3 +280,66 @@ impl From<FileSystemEdit> for SourceChange {
}
}
}

enum PlaceSnippet {
/// Place a tabstop before a node
Before(SyntaxNode),
/// Place a tabstop before a node
After(SyntaxNode),
/// Place a placeholder snippet in place of the node
Over(SyntaxNode),
}

impl PlaceSnippet {
/// Places the snippet before or over a node with the given tab stop index
fn place(self, order: usize) {
// ensure the target node is still attached
match &self {
PlaceSnippet::Before(node) | PlaceSnippet::After(node) | PlaceSnippet::Over(node) => {
// node should still be in the tree, but if it isn't
// then it's okay to just ignore this place
if stdx::never!(node.parent().is_none()) {
return;
}
}
}

match self {
PlaceSnippet::Before(node) => {
ted::insert_raw(ted::Position::before(&node), Self::make_tab_stop(order));
}
PlaceSnippet::After(node) => {
ted::insert_raw(ted::Position::after(&node), Self::make_tab_stop(order));
}
PlaceSnippet::Over(node) => {
let position = ted::Position::before(&node);
node.detach();

let snippet = ast::SourceFile::parse(&format!("${{{order}:_}}"))
.syntax_node()
.clone_for_update();

let placeholder =
snippet.descendants().find_map(ast::UnderscoreExpr::cast).unwrap();
ted::replace(placeholder.syntax(), node);

ted::insert_raw(position, snippet);
}
}
}

fn make_tab_stop(order: usize) -> SyntaxNode {
let stop = ast::SourceFile::parse(&format!("stop!(${order})"))
.syntax_node()
.descendants()
.find_map(ast::TokenTree::cast)
.unwrap()
.syntax()
.clone_for_update();

stop.first_token().unwrap().detach();
stop.last_token().unwrap().detach();

stop
}
}