Merge pull request #18921 from Veykril/push-zwullmxomvsm

internal: Compute inlay hint text edits lazily
This commit is contained in:
Lukas Wirth 2025-01-12 13:20:33 +00:00 committed by GitHub
commit 69ab0cfb48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 150 additions and 57 deletions

View file

@ -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:?}");
}

View file

@ -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) => (),
}
}

View file

@ -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

View file

@ -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()));
}

View file

@ -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

View file

@ -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,
}
"#]],
);
}
}

View file

@ -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()),
}
}

View file

@ -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,

View file

@ -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,