Unnamed repository; edit this file 'description' to name the repository.
Merge pull request #22261 from Shourya742/2026-05-03-migrate-missing-fields-to-syntax-editor
Migrate missing fields to syntax editor
Chayim Refael Friedman 11 days ago
parent ddc1508 · parent f80347b · commit 06aba0c
-rw-r--r--crates/ide-diagnostics/src/handlers/missing_fields.rs43
-rw-r--r--crates/syntax/src/ast/edit_in_place.rs181
-rw-r--r--crates/syntax/src/syntax_editor.rs11
-rw-r--r--crates/syntax/src/syntax_editor/edits.rs113
4 files changed, 151 insertions, 197 deletions
diff --git a/crates/ide-diagnostics/src/handlers/missing_fields.rs b/crates/ide-diagnostics/src/handlers/missing_fields.rs
index 206d8caf90..607f0cbd23 100644
--- a/crates/ide-diagnostics/src/handlers/missing_fields.rs
+++ b/crates/ide-diagnostics/src/handlers/missing_fields.rs
@@ -18,6 +18,7 @@ use stdx::format_to;
use syntax::{
AstNode, Edition, SyntaxNode, SyntaxNodePtr, ToSmolStr,
ast::{self, make},
+ syntax_editor::SyntaxEditor,
};
use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, fix};
@@ -115,16 +116,20 @@ fn fixes(ctx: &DiagnosticsContext<'_, '_>, d: &hir::MissingFields) -> Option<Vec
}
});
+ let old_field_list = field_list_parent.record_expr_field_list()?;
+ let root = old_field_list.syntax().ancestors().last()?;
+ let (editor, _) = SyntaxEditor::new(root);
+ let make = editor.make();
+
let generate_fill_expr = |ty: &Type<'_>| match ctx.config.expr_fill_default {
- ExprFillDefaultMode::Todo => make::ext::expr_todo(),
- ExprFillDefaultMode::Underscore => make::ext::expr_underscore(),
+ ExprFillDefaultMode::Todo => make.expr_todo(),
+ ExprFillDefaultMode::Underscore => make.expr_underscore().into(),
ExprFillDefaultMode::Default => {
- get_default_constructor(ctx, d, ty).unwrap_or_else(make::ext::expr_todo)
+ get_default_constructor(ctx, d, ty).unwrap_or_else(|| make.expr_todo())
}
};
- let old_field_list = field_list_parent.record_expr_field_list()?;
- let new_field_list = old_field_list.clone_for_update();
+ let mut new_fields = Vec::new();
for (f, ty) in missing_fields.iter() {
let field_expr = if let Some(local_candidate) = locals.get(&f.name(ctx.sema.db)) {
cov_mark::hit!(field_shorthand);
@@ -159,31 +164,39 @@ fn fixes(ctx: &DiagnosticsContext<'_, '_>, d: &hir::MissingFields) -> Option<Vec
if expr.is_some() { expr } else { Some(generate_fill_expr(ty)) }
};
- let field = make::record_expr_field(
- make::name_ref(&f.name(ctx.sema.db).display_no_db(ctx.edition).to_smolstr()),
+ let field = make.record_expr_field(
+ make.name_ref(&f.name(ctx.sema.db).display_no_db(ctx.edition).to_smolstr()),
field_expr,
);
- new_field_list.add_field(field.clone_for_update());
+ new_fields.push(field);
}
- build_text_edit(new_field_list.syntax(), old_field_list.syntax())
+ old_field_list.add_fields(&editor, new_fields);
+ let new_field_list = editor.finish().find_element(old_field_list.syntax())?;
+ build_text_edit(&new_field_list, old_field_list.syntax())
}
Either::Right(field_list_parent) => {
let missing_fields = ctx.sema.record_pattern_missing_fields(field_list_parent);
let old_field_list = field_list_parent.record_pat_field_list()?;
- let new_field_list = old_field_list.clone_for_update();
+ let root = old_field_list.syntax().ancestors().last()?;
+ let (editor, _) = SyntaxEditor::new(root);
+ let make = editor.make();
+
+ let mut new_fields = Vec::new();
for (f, _) in missing_fields.iter() {
- let field = make::record_pat_field_shorthand(
- make::ident_pat(
+ let field = make.record_pat_field_shorthand(
+ make.ident_pat(
false,
false,
- make::name(&f.name(ctx.sema.db).display_no_db(ctx.edition).to_smolstr()),
+ make.name(&f.name(ctx.sema.db).display_no_db(ctx.edition).to_smolstr()),
)
.into(),
);
- new_field_list.add_field(field.clone_for_update());
+ new_fields.push(field);
}
- build_text_edit(new_field_list.syntax(), old_field_list.syntax())
+ old_field_list.add_fields(&editor, new_fields);
+ let new_field_list = editor.finish().find_element(old_field_list.syntax())?;
+ build_text_edit(&new_field_list, old_field_list.syntax())
}
}
}
diff --git a/crates/syntax/src/ast/edit_in_place.rs b/crates/syntax/src/ast/edit_in_place.rs
index 7d7c875508..2b947f2d0f 100644
--- a/crates/syntax/src/ast/edit_in_place.rs
+++ b/crates/syntax/src/ast/edit_in_place.rs
@@ -2,15 +2,15 @@
use std::iter::{empty, once, successors};
-use parser::{SyntaxKind, T};
+use parser::T;
use crate::{
- AstNode, AstToken, Direction, SyntaxElement,
+ AstNode, AstToken, Direction,
SyntaxKind::{ATTR, COMMENT, WHITESPACE},
- SyntaxNode, SyntaxToken,
+ SyntaxNode,
algo::{self, neighbor},
ast::{self, edit::IndentLevel, make, syntax_factory::SyntaxFactory},
- syntax_editor::{self, SyntaxEditor},
+ syntax_editor::SyntaxEditor,
ted,
};
@@ -270,75 +270,6 @@ impl ast::Impl {
}
}
-impl ast::AssocItemList {
- /// Adds a new associated item after all of the existing associated items.
- ///
- /// Attention! This function does align the first line of `item` with respect to `self`,
- /// but it does _not_ change indentation of other lines (if any).
- pub fn add_item(&self, editor: &SyntaxEditor, item: ast::AssocItem) {
- let make = editor.make();
- let (indent, position, whitespace) = match self.assoc_items().last() {
- Some(last_item) => (
- IndentLevel::from_node(last_item.syntax()),
- syntax_editor::Position::after(last_item.syntax()),
- "\n\n",
- ),
- None => match self.l_curly_token() {
- Some(l_curly) => {
- normalize_ws_between_braces_with_editor(editor, self.syntax());
- (
- IndentLevel::from_token(&l_curly) + 1,
- syntax_editor::Position::after(&l_curly),
- "\n",
- )
- }
- None => (
- IndentLevel::zero(),
- syntax_editor::Position::last_child_of(self.syntax()),
- "\n",
- ),
- },
- };
- let elements: Vec<SyntaxElement> = vec![
- make.whitespace(&format!("{whitespace}{indent}")).into(),
- item.syntax().clone().into(),
- ];
- editor.insert_all(position, elements);
- }
-}
-
-impl ast::RecordExprFieldList {
- pub fn add_field(&self, field: ast::RecordExprField) {
- let is_multiline = self.syntax().text().contains_char('\n');
- let whitespace = if is_multiline {
- let indent = IndentLevel::from_node(self.syntax()) + 1;
- make::tokens::whitespace(&format!("\n{indent}"))
- } else {
- make::tokens::single_space()
- };
-
- if is_multiline {
- normalize_ws_between_braces(self.syntax());
- }
-
- let position = match self.fields().last() {
- Some(last_field) => {
- let comma = get_or_insert_comma_after(last_field.syntax());
- ted::Position::after(comma)
- }
- None => match self.l_curly_token() {
- Some(it) => ted::Position::after(it),
- None => ted::Position::last_child_of(self.syntax()),
- },
- };
-
- ted::insert_all(position, vec![whitespace.into(), field.syntax().clone().into()]);
- if is_multiline {
- ted::insert(ted::Position::after(field.syntax()), ast::make::token(T![,]));
- }
- }
-}
-
impl ast::RecordExprField {
/// This will either replace the initializer, or in the case that this is a shorthand convert
/// the initializer into the name ref and insert the expr as the new initializer.
@@ -360,110 +291,6 @@ impl ast::RecordExprField {
}
}
-impl ast::RecordPatFieldList {
- pub fn add_field(&self, field: ast::RecordPatField) {
- let is_multiline = self.syntax().text().contains_char('\n');
- let whitespace = if is_multiline {
- let indent = IndentLevel::from_node(self.syntax()) + 1;
- make::tokens::whitespace(&format!("\n{indent}"))
- } else {
- make::tokens::single_space()
- };
-
- if is_multiline {
- normalize_ws_between_braces(self.syntax());
- }
-
- let position = match self.fields().last() {
- Some(last_field) => {
- let syntax = last_field.syntax();
- let comma = get_or_insert_comma_after(syntax);
- ted::Position::after(comma)
- }
- None => match self.l_curly_token() {
- Some(it) => ted::Position::after(it),
- None => ted::Position::last_child_of(self.syntax()),
- },
- };
-
- ted::insert_all(position, vec![whitespace.into(), field.syntax().clone().into()]);
- if is_multiline {
- ted::insert(ted::Position::after(field.syntax()), ast::make::token(T![,]));
- }
- }
-}
-
-fn get_or_insert_comma_after(syntax: &SyntaxNode) -> SyntaxToken {
- match syntax
- .siblings_with_tokens(Direction::Next)
- .filter_map(|it| it.into_token())
- .find(|it| it.kind() == T![,])
- {
- Some(it) => it,
- None => {
- let comma = ast::make::token(T![,]);
- ted::insert(ted::Position::after(syntax), &comma);
- comma
- }
- }
-}
-
-fn normalize_ws_between_braces(node: &SyntaxNode) -> Option<()> {
- let l = node
- .children_with_tokens()
- .filter_map(|it| it.into_token())
- .find(|it| it.kind() == T!['{'])?;
- let r = node
- .children_with_tokens()
- .filter_map(|it| it.into_token())
- .find(|it| it.kind() == T!['}'])?;
-
- let indent = IndentLevel::from_node(node);
-
- match l.next_sibling_or_token() {
- Some(ws)
- if ws.kind() == SyntaxKind::WHITESPACE
- && ws.next_sibling_or_token()?.into_token()? == r =>
- {
- ted::replace(ws, make::tokens::whitespace(&format!("\n{indent}")));
- }
- Some(ws) if ws.kind() == T!['}'] => {
- ted::insert(ted::Position::after(l), make::tokens::whitespace(&format!("\n{indent}")));
- }
- _ => (),
- }
- Some(())
-}
-
-fn normalize_ws_between_braces_with_editor(editor: &SyntaxEditor, node: &SyntaxNode) -> Option<()> {
- let make = editor.make();
- let l = node
- .children_with_tokens()
- .filter_map(|it| it.into_token())
- .find(|it| it.kind() == T!['{'])?;
- let r = node
- .children_with_tokens()
- .filter_map(|it| it.into_token())
- .find(|it| it.kind() == T!['}'])?;
-
- let indent = IndentLevel::from_node(node);
-
- match l.next_sibling_or_token() {
- Some(ws)
- if ws.kind() == SyntaxKind::WHITESPACE
- && ws.next_sibling_or_token()?.into_token()? == r =>
- {
- editor.replace(ws, make.whitespace(&format!("\n{indent}")));
- }
- Some(ws) if ws.kind() == T!['}'] => {
- editor
- .insert(syntax_editor::Position::after(l), make.whitespace(&format!("\n{indent}")));
- }
- _ => (),
- }
- Some(())
-}
-
pub trait Indent: AstNode + Clone + Sized {
fn indent_level(&self) -> IndentLevel {
IndentLevel::from_node(self.syntax())
diff --git a/crates/syntax/src/syntax_editor.rs b/crates/syntax/src/syntax_editor.rs
index 704240e9ad..7f499a241b 100644
--- a/crates/syntax/src/syntax_editor.rs
+++ b/crates/syntax/src/syntax_editor.rs
@@ -216,6 +216,17 @@ impl SyntaxEdit {
pub fn find_annotation(&self, annotation: SyntaxAnnotation) -> &[SyntaxElement] {
self.annotations.get(&annotation).as_ref().map_or(&[], |it| it.as_slice())
}
+
+ pub fn find_element(&self, old_node: &SyntaxNode) -> Option<SyntaxNode> {
+ let old_root_start = self.old_root.text_range().start();
+ let old_start = old_node.text_range().start() - old_root_start;
+ let new_root_start = self.new_root.text_range().start();
+ let kind = old_node.kind();
+
+ self.new_root
+ .descendants()
+ .find(|it| it.kind() == kind && it.text_range().start() - new_root_start == old_start)
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
diff --git a/crates/syntax/src/syntax_editor/edits.rs b/crates/syntax/src/syntax_editor/edits.rs
index a684c0bfdb..0338d976b0 100644
--- a/crates/syntax/src/syntax_editor/edits.rs
+++ b/crates/syntax/src/syntax_editor/edits.rs
@@ -191,11 +191,7 @@ impl SyntaxEditor {
fn get_or_insert_comma_after(editor: &SyntaxEditor, syntax: &SyntaxNode) -> SyntaxToken {
let make = editor.make();
- match syntax
- .siblings_with_tokens(Direction::Next)
- .filter_map(|it| it.into_token())
- .find(|it| it.kind() == T![,])
- {
+ match comma_after(syntax) {
Some(it) => it,
None => {
let comma = make.token(T![,]);
@@ -241,6 +237,113 @@ impl ast::AssocItemList {
}
}
+impl ast::RecordExprFieldList {
+ pub fn add_fields(
+ &self,
+ editor: &SyntaxEditor,
+ fields: impl IntoIterator<Item = ast::RecordExprField>,
+ ) {
+ add_record_fields(
+ editor,
+ self.syntax(),
+ self.fields().last().map(|it| it.syntax().clone()),
+ self.l_curly_token(),
+ fields.into_iter().map(|it| it.syntax().clone().into()),
+ );
+ }
+}
+
+impl ast::RecordPatFieldList {
+ pub fn add_fields(
+ &self,
+ editor: &SyntaxEditor,
+ fields: impl IntoIterator<Item = ast::RecordPatField>,
+ ) {
+ add_record_fields(
+ editor,
+ self.syntax(),
+ self.fields().last().map(|it| it.syntax().clone()),
+ self.l_curly_token(),
+ fields.into_iter().map(|it| it.syntax().clone().into()),
+ );
+ }
+}
+
+fn add_record_fields(
+ editor: &SyntaxEditor,
+ field_list: &SyntaxNode,
+ last_field: Option<SyntaxNode>,
+ l_curly: Option<SyntaxToken>,
+ fields: impl Iterator<Item = SyntaxElement>,
+) {
+ let fields = fields.collect::<Vec<_>>();
+ if fields.is_empty() {
+ return;
+ }
+
+ let make = editor.make();
+ let is_multiline = field_list.text().contains_char('\n');
+ let whitespace = || {
+ if is_multiline {
+ let indent = IndentLevel::from_node(field_list) + 1;
+ make.whitespace(&format!("\n{indent}"))
+ } else {
+ make.whitespace(" ")
+ }
+ };
+
+ if is_multiline {
+ normalize_ws_between_braces(editor, field_list);
+ }
+
+ let mut elements = Vec::new();
+ let next_after_insert;
+ let position = match last_field {
+ Some(last_field) => match comma_after(&last_field) {
+ Some(comma) => {
+ next_after_insert = comma.next_sibling_or_token();
+ Position::after(comma)
+ }
+ None => {
+ next_after_insert = last_field.next_sibling_or_token();
+ elements.push(make.token(T![,]).into());
+ Position::after(last_field)
+ }
+ },
+ None => match l_curly {
+ Some(it) => {
+ next_after_insert = it.next_sibling_or_token();
+ Position::after(it)
+ }
+ None => {
+ next_after_insert = None;
+ Position::last_child_of(field_list)
+ }
+ },
+ };
+
+ let fields_len = fields.len();
+ for (idx, field) in fields.into_iter().enumerate() {
+ elements.push(whitespace().into());
+ elements.push(field);
+ if is_multiline || idx + 1 != fields_len {
+ elements.push(make.token(T![,]).into());
+ }
+ }
+ if !is_multiline && next_after_insert.is_some_and(|it| it.kind() != SyntaxKind::WHITESPACE) {
+ elements.push(make.whitespace(" ").into());
+ }
+
+ editor.insert_all(position, elements);
+}
+
+fn comma_after(syntax: &SyntaxNode) -> Option<SyntaxToken> {
+ syntax
+ .siblings_with_tokens(Direction::Next)
+ .filter_map(|it| it.into_token())
+ .find(|it| it.kind() == T![,])
+}
+
impl ast::Impl {
pub fn get_or_create_assoc_item_list_with_editor(
&self,