mirror of
				https://github.com/rust-lang/rust-analyzer.git
				synced 2025-10-31 12:04:43 +00:00 
			
		
		
		
	Merge pull request #18921 from Veykril/push-zwullmxomvsm
internal: Compute inlay hint text edits lazily
This commit is contained in:
		
						commit
						69ab0cfb48
					
				
					 9 changed files with 150 additions and 57 deletions
				
			
		|  | @ -1,6 +1,6 @@ | |||
| use std::{ | ||||
|     fmt::{self, Write}, | ||||
|     mem::take, | ||||
|     mem::{self, take}, | ||||
| }; | ||||
| 
 | ||||
| use either::Either; | ||||
|  | @ -297,6 +297,17 @@ pub struct InlayHintsConfig { | |||
|     pub closing_brace_hints_min_lines: Option<usize>, | ||||
|     pub fields_to_resolve: InlayFieldsToResolve, | ||||
| } | ||||
| impl InlayHintsConfig { | ||||
|     fn lazy_text_edit(&self, finish: impl FnOnce() -> TextEdit) -> Lazy<TextEdit> { | ||||
|         if self.fields_to_resolve.resolve_text_edits { | ||||
|             Lazy::Lazy | ||||
|         } else { | ||||
|             let edit = finish(); | ||||
|             never!(edit.is_empty(), "inlay hint produced an empty text edit"); | ||||
|             Lazy::Computed(edit) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Copy, Clone, Debug, PartialEq, Eq)] | ||||
| pub struct InlayFieldsToResolve { | ||||
|  | @ -408,12 +419,32 @@ pub struct InlayHint { | |||
|     /// The actual label to show in the inlay hint.
 | ||||
|     pub label: InlayHintLabel, | ||||
|     /// Text edit to apply when "accepting" this inlay hint.
 | ||||
|     pub text_edit: Option<TextEdit>, | ||||
|     pub text_edit: Option<Lazy<TextEdit>>, | ||||
|     /// Range to recompute inlay hints when trying to resolve for this hint. If this is none, the
 | ||||
|     /// hint does not support resolving.
 | ||||
|     pub resolve_parent: Option<TextRange>, | ||||
| } | ||||
| 
 | ||||
| /// A type signaling that a value is either computed, or is available for computation.
 | ||||
| #[derive(Clone, Debug)] | ||||
| pub enum Lazy<T> { | ||||
|     Computed(T), | ||||
|     Lazy, | ||||
| } | ||||
| 
 | ||||
| impl<T> Lazy<T> { | ||||
|     pub fn computed(self) -> Option<T> { | ||||
|         match self { | ||||
|             Lazy::Computed(it) => Some(it), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn is_lazy(&self) -> bool { | ||||
|         matches!(self, Self::Lazy) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::hash::Hash for InlayHint { | ||||
|     fn hash<H: std::hash::Hasher>(&self, state: &mut H) { | ||||
|         self.range.hash(state); | ||||
|  | @ -422,7 +453,7 @@ impl std::hash::Hash for InlayHint { | |||
|         self.pad_right.hash(state); | ||||
|         self.kind.hash(state); | ||||
|         self.label.hash(state); | ||||
|         self.text_edit.is_some().hash(state); | ||||
|         mem::discriminant(&self.text_edit).hash(state); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -439,10 +470,6 @@ impl InlayHint { | |||
|             resolve_parent: None, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn needs_resolve(&self) -> Option<TextRange> { | ||||
|         self.resolve_parent.filter(|_| self.text_edit.is_some() || self.label.needs_resolve()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Hash)] | ||||
|  | @ -503,10 +530,6 @@ impl InlayHintLabel { | |||
|         } | ||||
|         self.parts.push(part); | ||||
|     } | ||||
| 
 | ||||
|     pub fn needs_resolve(&self) -> bool { | ||||
|         self.parts.iter().any(|part| part.linked_location.is_some() || part.tooltip.is_some()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<String> for InlayHintLabel { | ||||
|  | @ -725,19 +748,22 @@ fn hint_iterator( | |||
| 
 | ||||
| fn ty_to_text_edit( | ||||
|     sema: &Semantics<'_, RootDatabase>, | ||||
|     config: &InlayHintsConfig, | ||||
|     node_for_hint: &SyntaxNode, | ||||
|     ty: &hir::Type, | ||||
|     offset_to_insert: TextSize, | ||||
|     prefix: String, | ||||
| ) -> Option<TextEdit> { | ||||
|     let scope = sema.scope(node_for_hint)?; | ||||
|     prefix: impl Into<String>, | ||||
| ) -> Option<Lazy<TextEdit>> { | ||||
|     // FIXME: Limit the length and bail out on excess somehow?
 | ||||
|     let rendered = ty.display_source_code(scope.db, scope.module().into(), false).ok()?; | ||||
| 
 | ||||
|     let mut builder = TextEdit::builder(); | ||||
|     builder.insert(offset_to_insert, prefix); | ||||
|     builder.insert(offset_to_insert, rendered); | ||||
|     Some(builder.finish()) | ||||
|     let rendered = sema | ||||
|         .scope(node_for_hint) | ||||
|         .and_then(|scope| ty.display_source_code(scope.db, scope.module().into(), false).ok())?; | ||||
|     Some(config.lazy_text_edit(|| { | ||||
|         let mut builder = TextEdit::builder(); | ||||
|         builder.insert(offset_to_insert, prefix.into()); | ||||
|         builder.insert(offset_to_insert, rendered); | ||||
|         builder.finish() | ||||
|     })) | ||||
| } | ||||
| 
 | ||||
| fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool { | ||||
|  | @ -847,7 +873,7 @@ mod tests { | |||
| 
 | ||||
|         let edits = inlay_hints | ||||
|             .into_iter() | ||||
|             .filter_map(|hint| hint.text_edit) | ||||
|             .filter_map(|hint| hint.text_edit?.computed()) | ||||
|             .reduce(|mut acc, next| { | ||||
|                 acc.union(next).expect("merging text edits failed"); | ||||
|                 acc | ||||
|  | @ -867,7 +893,8 @@ mod tests { | |||
|         let (analysis, file_id) = fixture::file(ra_fixture); | ||||
|         let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap(); | ||||
| 
 | ||||
|         let edits: Vec<_> = inlay_hints.into_iter().filter_map(|hint| hint.text_edit).collect(); | ||||
|         let edits: Vec<_> = | ||||
|             inlay_hints.into_iter().filter_map(|hint| hint.text_edit?.computed()).collect(); | ||||
| 
 | ||||
|         assert!(edits.is_empty(), "unexpected edits: {edits:?}"); | ||||
|     } | ||||
|  |  | |||
|  | @ -183,7 +183,7 @@ pub(super) fn hints( | |||
|         return None; | ||||
|     } | ||||
|     if allow_edit { | ||||
|         let edit = { | ||||
|         let edit = Some(config.lazy_text_edit(|| { | ||||
|             let mut b = TextEditBuilder::default(); | ||||
|             if let Some(pre) = &pre { | ||||
|                 b.insert( | ||||
|  | @ -198,14 +198,14 @@ pub(super) fn hints( | |||
|                 ); | ||||
|             } | ||||
|             b.finish() | ||||
|         }; | ||||
|         })); | ||||
|         match (&mut pre, &mut post) { | ||||
|             (Some(pre), Some(post)) => { | ||||
|                 pre.text_edit = Some(edit.clone()); | ||||
|                 post.text_edit = Some(edit); | ||||
|                 pre.text_edit = edit.clone(); | ||||
|                 post.text_edit = edit; | ||||
|             } | ||||
|             (Some(pre), None) => pre.text_edit = Some(edit), | ||||
|             (None, Some(post)) => post.text_edit = Some(edit), | ||||
|             (Some(pre), None) => pre.text_edit = edit, | ||||
|             (None, Some(post)) => post.text_edit = edit, | ||||
|             (None, None) => (), | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -78,13 +78,14 @@ pub(super) fn hints( | |||
|     let text_edit = if let Some(colon_token) = &type_ascriptable { | ||||
|         ty_to_text_edit( | ||||
|             sema, | ||||
|             config, | ||||
|             desc_pat.syntax(), | ||||
|             &ty, | ||||
|             colon_token | ||||
|                 .as_ref() | ||||
|                 .map_or_else(|| pat.syntax().text_range(), |t| t.text_range()) | ||||
|                 .end(), | ||||
|             if colon_token.is_some() { String::new() } else { String::from(": ") }, | ||||
|             if colon_token.is_some() { "" } else { ": " }, | ||||
|         ) | ||||
|     } else { | ||||
|         None | ||||
|  |  | |||
|  | @ -99,17 +99,24 @@ pub(super) fn hints( | |||
|     } | ||||
| 
 | ||||
|     if let hints @ [_, ..] = &mut acc[acc_base..] { | ||||
|         let mut edit = TextEditBuilder::default(); | ||||
|         for h in &mut *hints { | ||||
|             edit.insert( | ||||
|                 match h.position { | ||||
|                     InlayHintPosition::Before => h.range.start(), | ||||
|                     InlayHintPosition::After => h.range.end(), | ||||
|                 }, | ||||
|                 h.label.parts.iter().map(|p| &*p.text).chain(h.pad_right.then_some(" ")).collect(), | ||||
|             ); | ||||
|         } | ||||
|         let edit = edit.finish(); | ||||
|         let edit = config.lazy_text_edit(|| { | ||||
|             let mut edit = TextEditBuilder::default(); | ||||
|             for h in &mut *hints { | ||||
|                 edit.insert( | ||||
|                     match h.position { | ||||
|                         InlayHintPosition::Before => h.range.start(), | ||||
|                         InlayHintPosition::After => h.range.end(), | ||||
|                     }, | ||||
|                     h.label | ||||
|                         .parts | ||||
|                         .iter() | ||||
|                         .map(|p| &*p.text) | ||||
|                         .chain(h.pad_right.then_some(" ")) | ||||
|                         .collect(), | ||||
|                 ); | ||||
|             } | ||||
|             edit.finish() | ||||
|         }); | ||||
|         hints.iter_mut().for_each(|h| h.text_edit = Some(edit.clone())); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -52,13 +52,14 @@ pub(super) fn hints( | |||
|     let text_edit = if has_block_body { | ||||
|         ty_to_text_edit( | ||||
|             sema, | ||||
|             config, | ||||
|             closure.syntax(), | ||||
|             &ty, | ||||
|             arrow | ||||
|                 .as_ref() | ||||
|                 .map_or_else(|| param_list.syntax().text_range(), |t| t.text_range()) | ||||
|                 .end(), | ||||
|             if arrow.is_none() { String::from(" -> ") } else { String::new() }, | ||||
|             if arrow.is_none() { " -> " } else { "" }, | ||||
|         ) | ||||
|     } else { | ||||
|         None | ||||
|  |  | |||
|  | @ -36,13 +36,14 @@ pub(super) fn enum_hints( | |||
|         return None; | ||||
|     } | ||||
|     for variant in enum_.variant_list()?.variants() { | ||||
|         variant_hints(acc, sema, &enum_, &variant); | ||||
|         variant_hints(acc, config, sema, &enum_, &variant); | ||||
|     } | ||||
|     Some(()) | ||||
| } | ||||
| 
 | ||||
| fn variant_hints( | ||||
|     acc: &mut Vec<InlayHint>, | ||||
|     config: &InlayHintsConfig, | ||||
|     sema: &Semantics<'_, RootDatabase>, | ||||
|     enum_: &ast::Enum, | ||||
|     variant: &ast::Variant, | ||||
|  | @ -88,7 +89,9 @@ fn variant_hints( | |||
|         }, | ||||
|         kind: InlayKind::Discriminant, | ||||
|         label, | ||||
|         text_edit: d.ok().map(|val| TextEdit::insert(range.start(), format!("{eq_} {val}"))), | ||||
|         text_edit: d.ok().map(|val| { | ||||
|             config.lazy_text_edit(|| TextEdit::insert(range.end(), format!("{eq_} {val}"))) | ||||
|         }), | ||||
|         position: InlayHintPosition::After, | ||||
|         pad_left: false, | ||||
|         pad_right: false, | ||||
|  | @ -99,8 +102,10 @@ fn variant_hints( | |||
| } | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use expect_test::expect; | ||||
| 
 | ||||
|     use crate::inlay_hints::{ | ||||
|         tests::{check_with_config, DISABLED_CONFIG}, | ||||
|         tests::{check_edit, check_with_config, DISABLED_CONFIG}, | ||||
|         DiscriminantHints, InlayHintsConfig, | ||||
|     }; | ||||
| 
 | ||||
|  | @ -207,4 +212,33 @@ enum Enum { | |||
| "#,
 | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn edit() { | ||||
|         check_edit( | ||||
|             InlayHintsConfig { discriminant_hints: DiscriminantHints::Always, ..DISABLED_CONFIG }, | ||||
|             r#" | ||||
| #[repr(u8)] | ||||
| enum Enum { | ||||
|     Variant(), | ||||
|     Variant1, | ||||
|     Variant2 {}, | ||||
|     Variant3, | ||||
|     Variant5, | ||||
|     Variant6, | ||||
| } | ||||
| "#,
 | ||||
|             expect![[r#" | ||||
|                 #[repr(u8)] | ||||
|                 enum Enum { | ||||
|                     Variant() = 0, | ||||
|                     Variant1 = 1, | ||||
|                     Variant2 {} = 2, | ||||
|                     Variant3 = 3, | ||||
|                     Variant5 = 4, | ||||
|                     Variant6 = 5, | ||||
|                 } | ||||
|             "#]],
 | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ use crate::{InlayHint, InlayHintsConfig}; | |||
| pub(super) fn extern_block_hints( | ||||
|     acc: &mut Vec<InlayHint>, | ||||
|     FamousDefs(_sema, _): &FamousDefs<'_, '_>, | ||||
|     _config: &InlayHintsConfig, | ||||
|     config: &InlayHintsConfig, | ||||
|     _file_id: EditionedFileId, | ||||
|     extern_block: ast::ExternBlock, | ||||
| ) -> Option<()> { | ||||
|  | @ -23,7 +23,9 @@ pub(super) fn extern_block_hints( | |||
|         pad_right: true, | ||||
|         kind: crate::InlayKind::ExternUnsafety, | ||||
|         label: crate::InlayHintLabel::from("unsafe"), | ||||
|         text_edit: Some(TextEdit::insert(abi.syntax().text_range().start(), "unsafe ".to_owned())), | ||||
|         text_edit: Some(config.lazy_text_edit(|| { | ||||
|             TextEdit::insert(abi.syntax().text_range().start(), "unsafe ".to_owned()) | ||||
|         })), | ||||
|         resolve_parent: Some(extern_block.syntax().text_range()), | ||||
|     }); | ||||
|     Some(()) | ||||
|  | @ -32,7 +34,7 @@ pub(super) fn extern_block_hints( | |||
| pub(super) fn fn_hints( | ||||
|     acc: &mut Vec<InlayHint>, | ||||
|     FamousDefs(_sema, _): &FamousDefs<'_, '_>, | ||||
|     _config: &InlayHintsConfig, | ||||
|     config: &InlayHintsConfig, | ||||
|     _file_id: EditionedFileId, | ||||
|     fn_: &ast::Fn, | ||||
|     extern_block: &ast::ExternBlock, | ||||
|  | @ -42,14 +44,14 @@ pub(super) fn fn_hints( | |||
|         return None; | ||||
|     } | ||||
|     let fn_ = fn_.fn_token()?; | ||||
|     acc.push(item_hint(extern_block, fn_)); | ||||
|     acc.push(item_hint(config, extern_block, fn_)); | ||||
|     Some(()) | ||||
| } | ||||
| 
 | ||||
| pub(super) fn static_hints( | ||||
|     acc: &mut Vec<InlayHint>, | ||||
|     FamousDefs(_sema, _): &FamousDefs<'_, '_>, | ||||
|     _config: &InlayHintsConfig, | ||||
|     config: &InlayHintsConfig, | ||||
|     _file_id: EditionedFileId, | ||||
|     static_: &ast::Static, | ||||
|     extern_block: &ast::ExternBlock, | ||||
|  | @ -59,11 +61,15 @@ pub(super) fn static_hints( | |||
|         return None; | ||||
|     } | ||||
|     let static_ = static_.static_token()?; | ||||
|     acc.push(item_hint(extern_block, static_)); | ||||
|     acc.push(item_hint(config, extern_block, static_)); | ||||
|     Some(()) | ||||
| } | ||||
| 
 | ||||
| fn item_hint(extern_block: &ast::ExternBlock, token: SyntaxToken) -> InlayHint { | ||||
| fn item_hint( | ||||
|     config: &InlayHintsConfig, | ||||
|     extern_block: &ast::ExternBlock, | ||||
|     token: SyntaxToken, | ||||
| ) -> InlayHint { | ||||
|     InlayHint { | ||||
|         range: token.text_range(), | ||||
|         position: crate::InlayHintPosition::Before, | ||||
|  | @ -71,7 +77,7 @@ fn item_hint(extern_block: &ast::ExternBlock, token: SyntaxToken) -> InlayHint { | |||
|         pad_right: true, | ||||
|         kind: crate::InlayKind::ExternUnsafety, | ||||
|         label: crate::InlayHintLabel::from("unsafe"), | ||||
|         text_edit: { | ||||
|         text_edit: Some(config.lazy_text_edit(|| { | ||||
|             let mut builder = TextEdit::builder(); | ||||
|             builder.insert(token.text_range().start(), "unsafe ".to_owned()); | ||||
|             if extern_block.unsafe_token().is_none() { | ||||
|  | @ -79,8 +85,8 @@ fn item_hint(extern_block: &ast::ExternBlock, token: SyntaxToken) -> InlayHint { | |||
|                     builder.insert(abi.syntax().text_range().start(), "unsafe ".to_owned()); | ||||
|                 } | ||||
|             } | ||||
|             Some(builder.finish()) | ||||
|         }, | ||||
|             builder.finish() | ||||
|         })), | ||||
|         resolve_parent: Some(extern_block.syntax().text_range()), | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -39,7 +39,9 @@ pub(super) fn hints( | |||
|                 range: t.text_range(), | ||||
|                 kind: InlayKind::Lifetime, | ||||
|                 label: "'static".into(), | ||||
|                 text_edit: Some(TextEdit::insert(t.text_range().start(), "'static ".into())), | ||||
|                 text_edit: Some(config.lazy_text_edit(|| { | ||||
|                     TextEdit::insert(t.text_range().start(), "'static ".into()) | ||||
|                 })), | ||||
|                 position: InlayHintPosition::After, | ||||
|                 pad_left: false, | ||||
|                 pad_right: true, | ||||
|  |  | |||
|  | @ -547,7 +547,18 @@ pub(crate) fn inlay_hint( | |||
|     file_id: FileId, | ||||
|     mut inlay_hint: InlayHint, | ||||
| ) -> Cancellable<lsp_types::InlayHint> { | ||||
|     let resolve_range_and_hash = inlay_hint.needs_resolve().map(|range| { | ||||
|     let hint_needs_resolve = |hint: &InlayHint| -> Option<TextRange> { | ||||
|         hint.resolve_parent.filter(|_| { | ||||
|             hint.text_edit.is_some() | ||||
|                 || hint | ||||
|                     .label | ||||
|                     .parts | ||||
|                     .iter() | ||||
|                     .any(|part| part.linked_location.is_some() || part.tooltip.is_some()) | ||||
|         }) | ||||
|     }; | ||||
| 
 | ||||
|     let resolve_range_and_hash = hint_needs_resolve(&inlay_hint).map(|range| { | ||||
|         ( | ||||
|             range, | ||||
|             std::hash::BuildHasher::hash_one( | ||||
|  | @ -568,7 +579,11 @@ pub(crate) fn inlay_hint( | |||
|         something_to_resolve |= inlay_hint.text_edit.is_some(); | ||||
|         None | ||||
|     } else { | ||||
|         inlay_hint.text_edit.take().map(|it| text_edit_vec(line_index, it)) | ||||
|         inlay_hint | ||||
|             .text_edit | ||||
|             .take() | ||||
|             .and_then(|it| it.computed()) | ||||
|             .map(|it| text_edit_vec(line_index, it)) | ||||
|     }; | ||||
|     let (label, tooltip) = inlay_hint_label( | ||||
|         snap, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lukas Wirth
						Lukas Wirth