mirror of
https://github.com/oxalica/nil.git
synced 2025-10-06 00:32:51 +02:00
feat: inline expression code action (#173)
This commit is contained in:
147
crates/ide/src/ide/assists/inline.rs
Normal file
147
crates/ide/src/ide/assists/inline.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use super::{AssistKind, AssistsCtx};
|
||||||
|
use crate::def::{AstPtr, ResolveResult};
|
||||||
|
use crate::TextEdit;
|
||||||
|
use smol_str::{SmolStr, ToSmolStr};
|
||||||
|
use syntax::ast::AstNode;
|
||||||
|
use syntax::{ast, SyntaxNode};
|
||||||
|
|
||||||
|
pub(super) fn inline(ctx: &mut AssistsCtx<'_>) -> Option<()> {
|
||||||
|
let file_id = ctx.frange.file_id;
|
||||||
|
let parse = ctx.db.parse(file_id);
|
||||||
|
let name_res = ctx.db.name_resolution(file_id);
|
||||||
|
let source_map = ctx.db.source_map(file_id);
|
||||||
|
|
||||||
|
let mut rewrites: Vec<TextEdit> = vec![];
|
||||||
|
|
||||||
|
if let Some(usage) = ctx.covering_node::<ast::Ref>() {
|
||||||
|
let ptr = AstPtr::new(usage.syntax());
|
||||||
|
let expr_id = source_map.expr_for_node(ptr)?;
|
||||||
|
let &ResolveResult::Definition(name) = name_res.get(expr_id)? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let definition = {
|
||||||
|
let nodes = source_map.nodes_for_name(name).collect::<Vec<_>>();
|
||||||
|
// Only provide assist when there is only one node
|
||||||
|
// i.e. `let a.b = 1; a.c = 2; in a` is not supported
|
||||||
|
if let [ptr] = nodes.as_slice() {
|
||||||
|
ptr.to_node(&parse.syntax_node())
|
||||||
|
.ancestors()
|
||||||
|
.flat_map(ast::AttrpathValue::cast)
|
||||||
|
.find_map(|path_value| path_value.value())?
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rewrites.push(TextEdit {
|
||||||
|
delete: usage.syntax().text_range(),
|
||||||
|
insert: maybe_parenthesize(&definition, usage.syntax()),
|
||||||
|
});
|
||||||
|
} else if let Some(definition) = ctx.covering_node::<ast::Attr>() {
|
||||||
|
let ptr = AstPtr::new(definition.syntax());
|
||||||
|
let name_id = source_map.name_for_node(ptr)?;
|
||||||
|
let path_value = definition
|
||||||
|
.syntax()
|
||||||
|
.ancestors()
|
||||||
|
.find_map(ast::AttrpathValue::cast)?;
|
||||||
|
|
||||||
|
// Don't provide assist when there are more than one attrname
|
||||||
|
if path_value.attrpath()?.attrs().count() > 1 {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let definition = path_value.value()?;
|
||||||
|
|
||||||
|
let usages = name_res
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, res)| match res {
|
||||||
|
&ResolveResult::Definition(def) if def == name_id => source_map
|
||||||
|
.node_for_expr(id)
|
||||||
|
.map(|ptr| ptr.to_node(ctx.ast.syntax())),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let is_letin = ast::LetIn::cast(path_value.syntax().parent()?).is_some();
|
||||||
|
if is_letin {
|
||||||
|
rewrites.push(TextEdit {
|
||||||
|
delete: path_value.syntax().text_range(),
|
||||||
|
insert: Default::default(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for usage in usages {
|
||||||
|
rewrites.push(TextEdit {
|
||||||
|
delete: usage.text_range(),
|
||||||
|
insert: maybe_parenthesize(&definition, &usage),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.add(
|
||||||
|
"inline",
|
||||||
|
"Inline binding",
|
||||||
|
AssistKind::RefactorInline,
|
||||||
|
rewrites,
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parenthesize a node properly given the replacement context
|
||||||
|
fn maybe_parenthesize(replacement: &ast::Expr, original: &SyntaxNode) -> SmolStr {
|
||||||
|
let parent = original.parent().and_then(ast::Expr::cast);
|
||||||
|
let need_paren = matches!(parent, Some(outer) if !outer.contains_without_paren(replacement));
|
||||||
|
if need_paren {
|
||||||
|
format!("({})", replacement.syntax()).to_smolstr()
|
||||||
|
} else {
|
||||||
|
replacement.syntax().to_smolstr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use expect_test::expect;
|
||||||
|
define_check_assist!(super::inline);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn let_in_ref() {
|
||||||
|
check(
|
||||||
|
r#"let a = "foo"; in $0a"#,
|
||||||
|
expect![r#"let a = "foo"; in "foo""#],
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"let a = x: x; in $0a 1",
|
||||||
|
expect!["let a = x: x; in (x: x) 1"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn let_in_def() {
|
||||||
|
check("let $0a = x: x; in a a", expect!["let in (x: x) (x: x)"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_let_in_multi() {
|
||||||
|
check_no(r#"let a.b = "foo"; a.c = "bar"; in $0a"#);
|
||||||
|
check_no(r#"let a.b$0 = "foo"; a.c = "bar"; in a"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attr_ref() {
|
||||||
|
check(
|
||||||
|
"rec { foo = 1; bar = $0foo; }",
|
||||||
|
expect!["rec { foo = 1; bar = 1; }"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attr_def() {
|
||||||
|
check(
|
||||||
|
"rec { $0foo = 1; bar = foo; baz = foo; }",
|
||||||
|
expect!["rec { foo = 1; bar = 1; baz = 1; }"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -16,6 +16,7 @@ macro_rules! define_check_assist {
|
|||||||
mod add_to_top_level_lambda_param;
|
mod add_to_top_level_lambda_param;
|
||||||
mod convert_to_inherit;
|
mod convert_to_inherit;
|
||||||
mod flatten_attrset;
|
mod flatten_attrset;
|
||||||
|
mod inline;
|
||||||
mod pack_bindings;
|
mod pack_bindings;
|
||||||
mod remove_empty_inherit;
|
mod remove_empty_inherit;
|
||||||
mod remove_empty_let_in;
|
mod remove_empty_let_in;
|
||||||
@@ -40,6 +41,7 @@ pub struct Assist {
|
|||||||
pub enum AssistKind {
|
pub enum AssistKind {
|
||||||
QuickFix,
|
QuickFix,
|
||||||
RefactorRewrite,
|
RefactorRewrite,
|
||||||
|
RefactorInline,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn assists(db: &dyn DefDatabase, frange: FileRange) -> Vec<Assist> {
|
pub(crate) fn assists(db: &dyn DefDatabase, frange: FileRange) -> Vec<Assist> {
|
||||||
@@ -56,6 +58,7 @@ pub(crate) fn assists(db: &dyn DefDatabase, frange: FileRange) -> Vec<Assist> {
|
|||||||
rewrite_string::rewrite_string_to_indented,
|
rewrite_string::rewrite_string_to_indented,
|
||||||
rewrite_string::rewrite_uri_to_string,
|
rewrite_string::rewrite_uri_to_string,
|
||||||
rewrite_string::unquote_attr,
|
rewrite_string::unquote_attr,
|
||||||
|
inline::inline,
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut ctx = AssistsCtx::new(db, frange);
|
let mut ctx = AssistsCtx::new(db, frange);
|
||||||
|
@@ -302,6 +302,7 @@ pub(crate) fn to_code_action(vfs: &Vfs, assist: Assist) -> CodeActionOrCommand {
|
|||||||
kind: Some(match assist.kind {
|
kind: Some(match assist.kind {
|
||||||
AssistKind::QuickFix => CodeActionKind::QUICKFIX,
|
AssistKind::QuickFix => CodeActionKind::QUICKFIX,
|
||||||
AssistKind::RefactorRewrite => CodeActionKind::REFACTOR_REWRITE,
|
AssistKind::RefactorRewrite => CodeActionKind::REFACTOR_REWRITE,
|
||||||
|
AssistKind::RefactorInline => CodeActionKind::REFACTOR_INLINE,
|
||||||
}),
|
}),
|
||||||
diagnostics: None,
|
diagnostics: None,
|
||||||
edit: Some(to_workspace_edit(vfs, assist.edits)),
|
edit: Some(to_workspace_edit(vfs, assist.edits)),
|
||||||
|
@@ -167,3 +167,14 @@ https://nixos.org
|
|||||||
```nix
|
```nix
|
||||||
"https://nixos.org"
|
"https://nixos.org"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `inline`
|
||||||
|
|
||||||
|
Rewrite a binding to its definition.
|
||||||
|
```nix
|
||||||
|
let id = x: x; in a 1
|
||||||
|
```
|
||||||
|
=>
|
||||||
|
```nix
|
||||||
|
let id = x: x; in (x: x) 1
|
||||||
|
```
|
||||||
|
Reference in New Issue
Block a user