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
| -rw-r--r-- | crates/ide-diagnostics/src/handlers/missing_fields.rs | 43 | ||||
| -rw-r--r-- | crates/syntax/src/ast/edit_in_place.rs | 181 | ||||
| -rw-r--r-- | crates/syntax/src/syntax_editor.rs | 11 | ||||
| -rw-r--r-- | crates/syntax/src/syntax_editor/edits.rs | 113 |
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, |