Use a hash to find the correct inlay hint when resolving

This commit is contained in:
Lukas Wirth 2024-03-12 15:41:51 +01:00
parent 3115fd8b41
commit 4a93368590
7 changed files with 108 additions and 67 deletions

View file

@ -1,5 +1,6 @@
use std::{
fmt::{self, Write},
hash::{BuildHasher, BuildHasherDefault},
mem::take,
};
@ -8,7 +9,7 @@ use hir::{
known, ClosureStyle, HasVisibility, HirDisplay, HirDisplayError, HirWrite, ModuleDef,
ModuleDefId, Semantics,
};
use ide_db::{base_db::FileRange, famous_defs::FamousDefs, RootDatabase};
use ide_db::{base_db::FileRange, famous_defs::FamousDefs, FxHasher, RootDatabase};
use itertools::Itertools;
use smallvec::{smallvec, SmallVec};
use stdx::never;
@ -116,7 +117,7 @@ pub enum AdjustmentHintsMode {
PreferPostfix,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum InlayKind {
Adjustment,
BindingMode,
@ -132,7 +133,7 @@ pub enum InlayKind {
RangeExclusive,
}
#[derive(Debug)]
#[derive(Debug, Hash)]
pub enum InlayHintPosition {
Before,
After,
@ -153,6 +154,18 @@ pub struct InlayHint {
pub text_edit: Option<TextEdit>,
}
impl std::hash::Hash for InlayHint {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.range.hash(state);
self.position.hash(state);
self.pad_left.hash(state);
self.pad_right.hash(state);
self.kind.hash(state);
self.label.hash(state);
self.text_edit.is_some().hash(state);
}
}
impl InlayHint {
fn closing_paren_after(kind: InlayKind, range: TextRange) -> InlayHint {
InlayHint {
@ -183,13 +196,13 @@ impl InlayHint {
}
}
#[derive(Debug)]
#[derive(Debug, Hash)]
pub enum InlayTooltip {
String(String),
Markdown(String),
}
#[derive(Default)]
#[derive(Default, Hash)]
pub struct InlayHintLabel {
pub parts: SmallVec<[InlayHintLabelPart; 1]>,
}
@ -267,6 +280,7 @@ impl fmt::Debug for InlayHintLabel {
}
}
#[derive(Hash)]
pub struct InlayHintLabelPart {
pub text: String,
/// Source location represented by this label part. The client will use this to fetch the part's
@ -315,9 +329,7 @@ impl fmt::Write for InlayHintLabelBuilder<'_> {
impl HirWrite for InlayHintLabelBuilder<'_> {
fn start_location_link(&mut self, def: ModuleDefId) {
if self.location.is_some() {
never!("location link is already started");
}
never!(self.location.is_some(), "location link is already started");
self.make_new_part();
let Some(location) = ModuleDef::from(def).try_to_nav(self.db) else { return };
let location = location.call_site();
@ -427,11 +439,6 @@ fn ty_to_text_edit(
Some(builder.finish())
}
pub enum RangeLimit {
Fixed(TextRange),
NearestParent(TextSize),
}
// Feature: Inlay Hints
//
// rust-analyzer shows additional information inline with the source code.
@ -453,7 +460,7 @@ pub enum RangeLimit {
pub(crate) fn inlay_hints(
db: &RootDatabase,
file_id: FileId,
range_limit: Option<RangeLimit>,
range_limit: Option<TextRange>,
config: &InlayHintsConfig,
) -> Vec<InlayHint> {
let _p = tracing::span!(tracing::Level::INFO, "inlay_hints").entered();
@ -468,31 +475,13 @@ pub(crate) fn inlay_hints(
let hints = |node| hints(&mut acc, &famous_defs, config, file_id, node);
match range_limit {
Some(RangeLimit::Fixed(range)) => match file.covering_element(range) {
Some(range) => match file.covering_element(range) {
NodeOrToken::Token(_) => return acc,
NodeOrToken::Node(n) => n
.descendants()
.filter(|descendant| range.intersect(descendant.text_range()).is_some())
.for_each(hints),
},
Some(RangeLimit::NearestParent(position)) => {
match file.token_at_offset(position).left_biased() {
Some(token) => {
if let Some(parent_block) =
token.parent_ancestors().find_map(ast::BlockExpr::cast)
{
parent_block.syntax().descendants().for_each(hints)
} else if let Some(parent_item) =
token.parent_ancestors().find_map(ast::Item::cast)
{
parent_item.syntax().descendants().for_each(hints)
} else {
return acc;
}
}
None => return acc,
}
}
None => file.descendants().for_each(hints),
};
}
@ -500,6 +489,39 @@ pub(crate) fn inlay_hints(
acc
}
pub(crate) fn inlay_hints_resolve(
db: &RootDatabase,
file_id: FileId,
position: TextSize,
hash: u64,
config: &InlayHintsConfig,
) -> Option<InlayHint> {
let _p = tracing::span!(tracing::Level::INFO, "inlay_hints").entered();
let sema = Semantics::new(db);
let file = sema.parse(file_id);
let file = file.syntax();
let scope = sema.scope(file)?;
let famous_defs = FamousDefs(&sema, scope.krate());
let mut acc = Vec::new();
let hints = |node| hints(&mut acc, &famous_defs, config, file_id, node);
match file.token_at_offset(position).left_biased() {
Some(token) => {
if let Some(parent_block) = token.parent_ancestors().find_map(ast::BlockExpr::cast) {
parent_block.syntax().descendants().for_each(hints)
} else if let Some(parent_item) = token.parent_ancestors().find_map(ast::Item::cast) {
parent_item.syntax().descendants().for_each(hints)
} else {
return None;
}
}
None => return None,
}
acc.into_iter().find(|hint| BuildHasherDefault::<FxHasher>::default().hash_one(hint) == hash)
}
fn hints(
hints: &mut Vec<InlayHint>,
famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,

View file

@ -176,11 +176,7 @@ mod tests {
use syntax::{TextRange, TextSize};
use test_utils::extract_annotations;
use crate::{
fixture,
inlay_hints::{InlayHintsConfig, RangeLimit},
ClosureReturnTypeHints,
};
use crate::{fixture, inlay_hints::InlayHintsConfig, ClosureReturnTypeHints};
use crate::inlay_hints::tests::{
check, check_edit, check_no_edit, check_with_config, DISABLED_CONFIG, TEST_CONFIG,
@ -403,7 +399,7 @@ fn main() {
.inlay_hints(
&InlayHintsConfig { type_hints: true, ..DISABLED_CONFIG },
file_id,
Some(RangeLimit::Fixed(TextRange::new(TextSize::from(500), TextSize::from(600)))),
Some(TextRange::new(TextSize::from(500), TextSize::from(600))),
)
.unwrap();
let actual =

View file

@ -90,7 +90,7 @@ pub use crate::{
inlay_hints::{
AdjustmentHints, AdjustmentHintsMode, ClosureReturnTypeHints, DiscriminantHints,
InlayFieldsToResolve, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintPosition,
InlayHintsConfig, InlayKind, InlayTooltip, LifetimeElisionHints, RangeLimit,
InlayHintsConfig, InlayKind, InlayTooltip, LifetimeElisionHints,
},
join_lines::JoinLinesConfig,
markup::Markup,
@ -415,10 +415,19 @@ impl Analysis {
&self,
config: &InlayHintsConfig,
file_id: FileId,
range: Option<RangeLimit>,
range: Option<TextRange>,
) -> Cancellable<Vec<InlayHint>> {
self.with_db(|db| inlay_hints::inlay_hints(db, file_id, range, config))
}
pub fn inlay_hints_resolve(
&self,
config: &InlayHintsConfig,
file_id: FileId,
position: TextSize,
hash: u64,
) -> Cancellable<Option<InlayHint>> {
self.with_db(|db| inlay_hints::inlay_hints_resolve(db, file_id, position, hash, config))
}
/// Returns the set of folding ranges.
pub fn folding_ranges(&self, file_id: FileId) -> Cancellable<Vec<Fold>> {

View file

@ -12,8 +12,8 @@ use anyhow::Context;
use ide::{
AnnotationConfig, AssistKind, AssistResolveStrategy, Cancellable, FilePosition, FileRange,
HoverAction, HoverGotoTypeData, InlayFieldsToResolve, Query, RangeInfo, RangeLimit,
ReferenceCategory, Runnable, RunnableKind, SingleResolve, SourceChange, TextEdit,
HoverAction, HoverGotoTypeData, InlayFieldsToResolve, Query, RangeInfo, ReferenceCategory,
Runnable, RunnableKind, SingleResolve, SourceChange, TextEdit,
};
use ide_db::SymbolKind;
use itertools::Itertools;
@ -1465,7 +1465,7 @@ pub(crate) fn handle_inlay_hints(
let inlay_hints_config = snap.config.inlay_hints();
Ok(Some(
snap.analysis
.inlay_hints(&inlay_hints_config, file_id, Some(RangeLimit::Fixed(range)))?
.inlay_hints(&inlay_hints_config, file_id, Some(range))?
.into_iter()
.map(|it| {
to_proto::inlay_hint(
@ -1499,10 +1499,11 @@ pub(crate) fn handle_inlay_hints_resolve(
let hint_position = from_proto::offset(&line_index, original_hint.position)?;
let mut forced_resolve_inlay_hints_config = snap.config.inlay_hints();
forced_resolve_inlay_hints_config.fields_to_resolve = InlayFieldsToResolve::empty();
let resolve_hints = snap.analysis.inlay_hints(
let resolve_hints = snap.analysis.inlay_hints_resolve(
&forced_resolve_inlay_hints_config,
file_id,
Some(RangeLimit::NearestParent(hint_position)),
hint_position,
resolve_data.hash,
)?;
let mut resolved_hints = resolve_hints

View file

@ -800,6 +800,7 @@ pub struct CompletionResolveData {
#[derive(Debug, Serialize, Deserialize)]
pub struct InlayHintResolveData {
pub file_id: u32,
pub hash: u64,
}
#[derive(Debug, Serialize, Deserialize)]

View file

@ -13,7 +13,7 @@ use ide::{
NavigationTarget, ReferenceCategory, RenameError, Runnable, Severity, SignatureHelp,
SnippetEdit, SourceChange, StructureNodeKind, SymbolKind, TextEdit, TextRange, TextSize,
};
use ide_db::rust_doc::format_docs;
use ide_db::{rust_doc::format_docs, FxHasher};
use itertools::Itertools;
use semver::VersionReq;
use serde_json::to_value;
@ -444,30 +444,42 @@ pub(crate) fn inlay_hint(
fields_to_resolve: &InlayFieldsToResolve,
line_index: &LineIndex,
file_id: FileId,
inlay_hint: InlayHint,
mut inlay_hint: InlayHint,
) -> Cancellable<lsp_types::InlayHint> {
let needs_resolve = inlay_hint.needs_resolve();
let (label, tooltip, mut something_to_resolve) =
inlay_hint_label(snap, fields_to_resolve, needs_resolve, inlay_hint.label)?;
let resolve_hash = inlay_hint.needs_resolve().then(|| {
std::hash::BuildHasher::hash_one(
&std::hash::BuildHasherDefault::<FxHasher>::default(),
&inlay_hint,
)
});
let mut something_to_resolve = false;
let text_edits = if snap
.config
.visual_studio_code_version()
// https://github.com/microsoft/vscode/issues/193124
.map_or(true, |version| VersionReq::parse(">=1.86.0").unwrap().matches(version))
&& needs_resolve
&& resolve_hash.is_some()
&& fields_to_resolve.resolve_text_edits
{
something_to_resolve |= inlay_hint.text_edit.is_some();
None
} else {
inlay_hint.text_edit.map(|it| text_edit_vec(line_index, it))
inlay_hint.text_edit.take().map(|it| text_edit_vec(line_index, it))
};
let (label, tooltip) = inlay_hint_label(
snap,
fields_to_resolve,
&mut something_to_resolve,
resolve_hash.is_some(),
inlay_hint.label,
)?;
let data = if needs_resolve && something_to_resolve {
Some(to_value(lsp_ext::InlayHintResolveData { file_id: file_id.index() }).unwrap())
} else {
None
let data = match resolve_hash {
Some(hash) if something_to_resolve => Some(
to_value(lsp_ext::InlayHintResolveData { file_id: file_id.index(), hash }).unwrap(),
),
_ => None,
};
Ok(lsp_types::InlayHint {
@ -492,15 +504,15 @@ pub(crate) fn inlay_hint(
fn inlay_hint_label(
snap: &GlobalStateSnapshot,
fields_to_resolve: &InlayFieldsToResolve,
something_to_resolve: &mut bool,
needs_resolve: bool,
mut label: InlayHintLabel,
) -> Cancellable<(lsp_types::InlayHintLabel, Option<lsp_types::InlayHintTooltip>, bool)> {
let mut something_to_resolve = false;
) -> Cancellable<(lsp_types::InlayHintLabel, Option<lsp_types::InlayHintTooltip>)> {
let (label, tooltip) = match &*label.parts {
[InlayHintLabelPart { linked_location: None, .. }] => {
let InlayHintLabelPart { text, tooltip, .. } = label.parts.pop().unwrap();
let hint_tooltip = if needs_resolve && fields_to_resolve.resolve_hint_tooltip {
something_to_resolve |= tooltip.is_some();
*something_to_resolve |= tooltip.is_some();
None
} else {
match tooltip {
@ -524,7 +536,7 @@ fn inlay_hint_label(
.into_iter()
.map(|part| {
let tooltip = if needs_resolve && fields_to_resolve.resolve_label_tooltip {
something_to_resolve |= part.tooltip.is_some();
*something_to_resolve |= part.tooltip.is_some();
None
} else {
match part.tooltip {
@ -543,7 +555,7 @@ fn inlay_hint_label(
}
};
let location = if needs_resolve && fields_to_resolve.resolve_label_location {
something_to_resolve |= part.linked_location.is_some();
*something_to_resolve |= part.linked_location.is_some();
None
} else {
part.linked_location.map(|range| location(snap, range)).transpose()?
@ -559,7 +571,7 @@ fn inlay_hint_label(
(lsp_types::InlayHintLabel::LabelParts(parts), None)
}
};
Ok((label, tooltip, something_to_resolve))
Ok((label, tooltip))
}
static TOKEN_RESULT_COUNTER: AtomicU32 = AtomicU32::new(1);

View file

@ -1,5 +1,5 @@
<!---
lsp/ext.rs hash: 61f485497d6e8e88
lsp/ext.rs hash: d5febcbf63650753
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue: