|
1 |
| -use hir::{db::ExpandDatabase, HirDisplay, InFile}; |
| 1 | +use hir::{db::ExpandDatabase, Adt, HasSource, HirDisplay, InFile}; |
2 | 2 | use ide_db::{
|
3 | 3 | assists::{Assist, AssistId, AssistKind},
|
4 | 4 | base_db::FileRange,
|
| 5 | + helpers::is_editable_crate, |
5 | 6 | label::Label,
|
6 |
| - source_change::SourceChange, |
| 7 | + source_change::{SourceChange, SourceChangeBuilder}, |
| 8 | +}; |
| 9 | +use syntax::{ |
| 10 | + ast::{self, edit::IndentLevel, make}, |
| 11 | + AstNode, AstPtr, SyntaxKind, |
7 | 12 | };
|
8 |
| -use syntax::{ast, AstNode, AstPtr}; |
9 | 13 | use text_edit::TextEdit;
|
10 | 14 |
|
11 | 15 | use crate::{adjusted_display_range, Diagnostic, DiagnosticCode, DiagnosticsContext};
|
@@ -47,14 +51,89 @@ pub(crate) fn unresolved_field(
|
47 | 51 |
|
48 | 52 | fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Vec<Assist>> {
|
49 | 53 | if d.method_with_same_name_exists {
|
50 |
| - method_fix(ctx, &d.expr) |
| 54 | + let mut method_fix = method_fix(ctx, &d.expr).unwrap_or_default(); |
| 55 | + method_fix.push(add_field_fix(ctx, d)?); |
| 56 | + Some(method_fix) |
51 | 57 | } else {
|
52 |
| - // FIXME: add quickfix |
53 |
| - |
54 |
| - None |
| 58 | + Some(vec![add_field_fix(ctx, d)?]) |
55 | 59 | }
|
56 | 60 | }
|
57 | 61 |
|
| 62 | +fn add_field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Assist> { |
| 63 | + // Get the FileRange of the invalid field access |
| 64 | + let root = ctx.sema.db.parse_or_expand(d.expr.file_id); |
| 65 | + let expr = d.expr.value.to_node(&root); |
| 66 | + |
| 67 | + let error_range = ctx.sema.original_range_opt(expr.syntax())?; |
| 68 | + // Convert the receiver to an ADT |
| 69 | + let adt = d.receiver.as_adt()?; |
| 70 | + let Adt::Struct(adt) = adt else { |
| 71 | + return None; |
| 72 | + }; |
| 73 | + |
| 74 | + let target_module = adt.module(ctx.sema.db); |
| 75 | + |
| 76 | + let suggested_type = |
| 77 | + if let Some(new_field_type) = ctx.sema.type_of_expr(&expr).map(|v| v.adjusted()) { |
| 78 | + let display = |
| 79 | + new_field_type.display_source_code(ctx.sema.db, target_module.into(), true).ok(); |
| 80 | + make::ty(display.as_deref().unwrap_or_else(|| "()")) |
| 81 | + } else { |
| 82 | + make::ty("()") |
| 83 | + }; |
| 84 | + |
| 85 | + if !is_editable_crate(target_module.krate(), ctx.sema.db) { |
| 86 | + return None; |
| 87 | + } |
| 88 | + let adt_source = adt.source(ctx.sema.db)?; |
| 89 | + let adt_syntax = adt_source.syntax(); |
| 90 | + let range = adt_syntax.original_file_range(ctx.sema.db); |
| 91 | + |
| 92 | + // Get range of final field in the struct |
| 93 | + let (offset, needs_comma, indent) = match adt.fields(ctx.sema.db).last() { |
| 94 | + Some(field) => { |
| 95 | + let last_field = field.source(ctx.sema.db)?.value; |
| 96 | + let hir::FieldSource::Named(record_field) = last_field else { |
| 97 | + return None; |
| 98 | + }; |
| 99 | + let last_field_syntax = record_field.syntax(); |
| 100 | + let last_field_imdent = IndentLevel::from_node(last_field_syntax); |
| 101 | + ( |
| 102 | + last_field_syntax.text_range().end(), |
| 103 | + !last_field_syntax.to_string().ends_with(','), |
| 104 | + last_field_imdent, |
| 105 | + ) |
| 106 | + } |
| 107 | + None => { |
| 108 | + // Empty Struct. Add a field right before the closing brace |
| 109 | + let indent = IndentLevel::from_node(&adt_syntax.value) + 1; |
| 110 | + let record_field_list = |
| 111 | + adt_syntax.value.children().find(|v| v.kind() == SyntaxKind::RECORD_FIELD_LIST)?; |
| 112 | + let offset = record_field_list.first_token().map(|f| f.text_range().end())?; |
| 113 | + (offset, false, indent) |
| 114 | + } |
| 115 | + }; |
| 116 | + |
| 117 | + let field_name = make::name(d.name.as_str()?); |
| 118 | + |
| 119 | + // If the Type is in the same file. We don't need to add a visibility modifier. Otherwise make it pub(crate) |
| 120 | + let visibility = if error_range.file_id == range.file_id { "" } else { "pub(crate)" }; |
| 121 | + let mut src_change_builder = SourceChangeBuilder::new(range.file_id); |
| 122 | + let comma = if needs_comma { "," } else { "" }; |
| 123 | + src_change_builder |
| 124 | + .insert(offset, format!("{comma}\n{indent}{visibility}{field_name}: {suggested_type}\n")); |
| 125 | + |
| 126 | + // FIXME: Add a Snippet for the new field type |
| 127 | + let source_change = src_change_builder.finish(); |
| 128 | + Some(Assist { |
| 129 | + id: AssistId("add-field-to-type", AssistKind::QuickFix), |
| 130 | + label: Label::new("Add field to type".to_owned()), |
| 131 | + group: None, |
| 132 | + target: error_range.range, |
| 133 | + source_change: Some(source_change), |
| 134 | + trigger_signature_help: false, |
| 135 | + }) |
| 136 | +} |
58 | 137 | // FIXME: We should fill out the call here, move the cursor and trigger signature help
|
59 | 138 | fn method_fix(
|
60 | 139 | ctx: &DiagnosticsContext<'_>,
|
|
0 commit comments