inlay_hint_cache.rs

   1use std::{cmp, ops::Range, sync::Arc};
   2
   3use crate::{
   4    display_map::Inlay, editor_settings, Anchor, Editor, ExcerptId, InlayId, MultiBuffer,
   5    MultiBufferSnapshot,
   6};
   7use anyhow::Context;
   8use clock::Global;
   9use gpui::{ModelHandle, Task, ViewContext};
  10use language::{Buffer, BufferSnapshot};
  11use log::error;
  12use parking_lot::RwLock;
  13use project::{InlayHint, InlayHintKind};
  14
  15use collections::{hash_map, HashMap, HashSet};
  16use util::post_inc;
  17
  18pub struct InlayHintCache {
  19    pub hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
  20    pub allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
  21    pub version: usize,
  22    update_tasks: HashMap<ExcerptId, UpdateTask>,
  23}
  24
  25struct UpdateTask {
  26    current: (InvalidationStrategy, SpawnedTask),
  27    pending_refresh: Option<SpawnedTask>,
  28}
  29
  30struct SpawnedTask {
  31    version: usize,
  32    is_running_rx: smol::channel::Receiver<()>,
  33    _task: Task<()>,
  34}
  35
  36#[derive(Debug)]
  37pub struct CachedExcerptHints {
  38    version: usize,
  39    buffer_version: Global,
  40    pub hints: Vec<(InlayId, InlayHint)>,
  41}
  42
  43#[derive(Debug, Clone, Copy)]
  44struct ExcerptQuery {
  45    buffer_id: u64,
  46    excerpt_id: ExcerptId,
  47    dimensions: ExcerptDimensions,
  48    cache_version: usize,
  49    invalidate: InvalidationStrategy,
  50}
  51
  52#[derive(Debug, Clone, Copy)]
  53struct ExcerptDimensions {
  54    excerpt_range_start: language::Anchor,
  55    excerpt_range_end: language::Anchor,
  56    excerpt_visible_range_start: language::Anchor,
  57    excerpt_visible_range_end: language::Anchor,
  58}
  59
  60impl ExcerptQuery {
  61    fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges {
  62        let visible_range =
  63            self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end;
  64        let mut other_ranges = Vec::new();
  65        if self
  66            .dimensions
  67            .excerpt_range_start
  68            .cmp(&self.dimensions.excerpt_visible_range_start, buffer)
  69            .is_lt()
  70        {
  71            let mut end = self.dimensions.excerpt_visible_range_start;
  72            end.offset -= 1;
  73            other_ranges.push(self.dimensions.excerpt_range_start..end);
  74        }
  75        if self
  76            .dimensions
  77            .excerpt_range_end
  78            .cmp(&self.dimensions.excerpt_visible_range_end, buffer)
  79            .is_gt()
  80        {
  81            let mut start = self.dimensions.excerpt_visible_range_end;
  82            start.offset += 1;
  83            other_ranges.push(start..self.dimensions.excerpt_range_end);
  84        }
  85
  86        HintFetchRanges {
  87            visible_range,
  88            other_ranges: other_ranges.into_iter().map(|range| range).collect(),
  89        }
  90    }
  91}
  92
  93impl UpdateTask {
  94    fn new(invalidation_strategy: InvalidationStrategy, spawned_task: SpawnedTask) -> Self {
  95        Self {
  96            current: (invalidation_strategy, spawned_task),
  97            pending_refresh: None,
  98        }
  99    }
 100
 101    fn is_running(&self) -> bool {
 102        !self.current.1.is_running_rx.is_closed()
 103            || self
 104                .pending_refresh
 105                .as_ref()
 106                .map_or(false, |task| !task.is_running_rx.is_closed())
 107    }
 108
 109    fn cache_version(&self) -> usize {
 110        self.current.1.version
 111    }
 112
 113    fn invalidation_strategy(&self) -> InvalidationStrategy {
 114        self.current.0
 115    }
 116}
 117
 118#[derive(Debug, Clone, Copy)]
 119pub enum InvalidationStrategy {
 120    Forced,
 121    OnConflict,
 122    None,
 123}
 124
 125#[derive(Debug, Default)]
 126pub struct InlaySplice {
 127    pub to_remove: Vec<InlayId>,
 128    pub to_insert: Vec<(Anchor, InlayId, InlayHint)>,
 129}
 130
 131#[derive(Debug)]
 132struct ExcerptHintsUpdate {
 133    excerpt_id: ExcerptId,
 134    cache_version: usize,
 135    remove_from_visible: Vec<InlayId>,
 136    remove_from_cache: HashSet<InlayId>,
 137    add_to_cache: HashSet<InlayHint>,
 138}
 139
 140impl InlayHintCache {
 141    pub fn new(inlay_hint_settings: editor_settings::InlayHints) -> Self {
 142        Self {
 143            allowed_hint_kinds: allowed_hint_types(inlay_hint_settings),
 144            hints: HashMap::default(),
 145            update_tasks: HashMap::default(),
 146            version: 0,
 147        }
 148    }
 149
 150    pub fn update_settings(
 151        &mut self,
 152        multi_buffer: &ModelHandle<MultiBuffer>,
 153        inlay_hint_settings: editor_settings::InlayHints,
 154        visible_hints: Vec<Inlay>,
 155        cx: &mut ViewContext<Editor>,
 156    ) -> Option<InlaySplice> {
 157        let new_allowed_hint_kinds = allowed_hint_types(inlay_hint_settings);
 158        if !inlay_hint_settings.enabled {
 159            if self.hints.is_empty() {
 160                self.allowed_hint_kinds = new_allowed_hint_kinds;
 161                None
 162            } else {
 163                self.clear();
 164                self.allowed_hint_kinds = new_allowed_hint_kinds;
 165                Some(InlaySplice {
 166                    to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
 167                    to_insert: Vec::new(),
 168                })
 169            }
 170        } else if new_allowed_hint_kinds == self.allowed_hint_kinds {
 171            None
 172        } else {
 173            let new_splice = self.new_allowed_hint_kinds_splice(
 174                multi_buffer,
 175                &visible_hints,
 176                &new_allowed_hint_kinds,
 177                cx,
 178            );
 179            if new_splice.is_some() {
 180                self.version += 1;
 181                self.update_tasks.clear();
 182                self.allowed_hint_kinds = new_allowed_hint_kinds;
 183            }
 184            new_splice
 185        }
 186    }
 187
 188    pub fn refresh_inlay_hints(
 189        &mut self,
 190        mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
 191        invalidate: InvalidationStrategy,
 192        cx: &mut ViewContext<Editor>,
 193    ) {
 194        let update_tasks = &mut self.update_tasks;
 195        let invalidate_cache = matches!(
 196            invalidate,
 197            InvalidationStrategy::Forced | InvalidationStrategy::OnConflict
 198        );
 199        if invalidate_cache {
 200            update_tasks
 201                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
 202        }
 203        let cache_version = self.version;
 204        excerpts_to_query.retain(|visible_excerpt_id, _| {
 205            match update_tasks.entry(*visible_excerpt_id) {
 206                hash_map::Entry::Occupied(o) => match o.get().cache_version().cmp(&cache_version) {
 207                    cmp::Ordering::Less => true,
 208                    cmp::Ordering::Equal => invalidate_cache,
 209                    cmp::Ordering::Greater => false,
 210                },
 211                hash_map::Entry::Vacant(_) => true,
 212            }
 213        });
 214
 215        cx.spawn(|editor, mut cx| async move {
 216            editor
 217                .update(&mut cx, |editor, cx| {
 218                    spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
 219                })
 220                .ok();
 221        })
 222        .detach();
 223    }
 224
 225    fn new_allowed_hint_kinds_splice(
 226        &self,
 227        multi_buffer: &ModelHandle<MultiBuffer>,
 228        visible_hints: &[Inlay],
 229        new_kinds: &HashSet<Option<InlayHintKind>>,
 230        cx: &mut ViewContext<Editor>,
 231    ) -> Option<InlaySplice> {
 232        let old_kinds = &self.allowed_hint_kinds;
 233        if new_kinds == old_kinds {
 234            return None;
 235        }
 236
 237        let mut to_remove = Vec::new();
 238        let mut to_insert = Vec::new();
 239        let mut shown_hints_to_remove = visible_hints.iter().fold(
 240            HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
 241            |mut current_hints, inlay| {
 242                current_hints
 243                    .entry(inlay.position.excerpt_id)
 244                    .or_default()
 245                    .push((inlay.position, inlay.id));
 246                current_hints
 247            },
 248        );
 249
 250        let multi_buffer = multi_buffer.read(cx);
 251        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
 252
 253        for (excerpt_id, excerpt_cached_hints) in &self.hints {
 254            let shown_excerpt_hints_to_remove =
 255                shown_hints_to_remove.entry(*excerpt_id).or_default();
 256            let excerpt_cached_hints = excerpt_cached_hints.read();
 257            let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
 258            shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
 259                let Some(buffer) = shown_anchor
 260                    .buffer_id
 261                    .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
 262                let buffer_snapshot = buffer.read(cx).snapshot();
 263                loop {
 264                    match excerpt_cache.peek() {
 265                        Some((cached_hint_id, cached_hint)) => {
 266                            if cached_hint_id == shown_hint_id {
 267                                excerpt_cache.next();
 268                                return !new_kinds.contains(&cached_hint.kind);
 269                            }
 270
 271                            match cached_hint
 272                                .position
 273                                .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
 274                            {
 275                                cmp::Ordering::Less | cmp::Ordering::Equal => {
 276                                    if !old_kinds.contains(&cached_hint.kind)
 277                                        && new_kinds.contains(&cached_hint.kind)
 278                                    {
 279                                        to_insert.push((
 280                                            multi_buffer_snapshot.anchor_in_excerpt(
 281                                                *excerpt_id,
 282                                                cached_hint.position,
 283                                            ),
 284                                            *cached_hint_id,
 285                                            cached_hint.clone(),
 286                                        ));
 287                                    }
 288                                    excerpt_cache.next();
 289                                }
 290                                cmp::Ordering::Greater => return true,
 291                            }
 292                        }
 293                        None => return true,
 294                    }
 295                }
 296            });
 297
 298            for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
 299                let cached_hint_kind = maybe_missed_cached_hint.kind;
 300                if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
 301                    to_insert.push((
 302                        multi_buffer_snapshot
 303                            .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
 304                        *cached_hint_id,
 305                        maybe_missed_cached_hint.clone(),
 306                    ));
 307                }
 308            }
 309        }
 310
 311        to_remove.extend(
 312            shown_hints_to_remove
 313                .into_values()
 314                .flatten()
 315                .map(|(_, hint_id)| hint_id),
 316        );
 317        if to_remove.is_empty() && to_insert.is_empty() {
 318            None
 319        } else {
 320            Some(InlaySplice {
 321                to_remove,
 322                to_insert,
 323            })
 324        }
 325    }
 326
 327    fn clear(&mut self) {
 328        self.version += 1;
 329        self.update_tasks.clear();
 330        self.hints.clear();
 331        self.allowed_hint_kinds.clear();
 332    }
 333}
 334
 335fn spawn_new_update_tasks(
 336    editor: &mut Editor,
 337    excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
 338    invalidation_strategy: InvalidationStrategy,
 339    update_cache_version: usize,
 340    cx: &mut ViewContext<'_, '_, Editor>,
 341) {
 342    let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
 343    for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query {
 344        if !excerpt_visible_range.is_empty() {
 345            let buffer = buffer_handle.read(cx);
 346            let buffer_snapshot = buffer.snapshot();
 347            let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
 348            let cache_is_empty = match &cached_excerpt_hints {
 349                Some(cached_excerpt_hints) => {
 350                    let new_task_buffer_version = buffer_snapshot.version();
 351                    let cached_excerpt_hints = cached_excerpt_hints.read();
 352                    let cached_buffer_version = &cached_excerpt_hints.buffer_version;
 353                    if cached_excerpt_hints.version > update_cache_version
 354                        || cached_buffer_version.changed_since(new_task_buffer_version)
 355                    {
 356                        return;
 357                    }
 358                    if !new_task_buffer_version.changed_since(&cached_buffer_version)
 359                        && !matches!(invalidation_strategy, InvalidationStrategy::Forced)
 360                    {
 361                        return;
 362                    }
 363
 364                    cached_excerpt_hints.hints.is_empty()
 365                }
 366                None => true,
 367            };
 368
 369            let buffer_id = buffer.remote_id();
 370            let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
 371            let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
 372
 373            let (multi_buffer_snapshot, full_excerpt_range) =
 374                editor.buffer.update(cx, |multi_buffer, cx| {
 375                    let multi_buffer_snapshot = multi_buffer.snapshot(cx);
 376                    (
 377                        multi_buffer_snapshot,
 378                        multi_buffer
 379                            .excerpts_for_buffer(&buffer_handle, cx)
 380                            .into_iter()
 381                            .find(|(id, _)| id == &excerpt_id)
 382                            .map(|(_, range)| range.context),
 383                    )
 384                });
 385
 386            if let Some(full_excerpt_range) = full_excerpt_range {
 387                let query = ExcerptQuery {
 388                    buffer_id,
 389                    excerpt_id,
 390                    dimensions: ExcerptDimensions {
 391                        excerpt_range_start: full_excerpt_range.start,
 392                        excerpt_range_end: full_excerpt_range.end,
 393                        excerpt_visible_range_start,
 394                        excerpt_visible_range_end,
 395                    },
 396                    cache_version: update_cache_version,
 397                    invalidate: invalidation_strategy,
 398                };
 399
 400                let new_update_task = |previous_task| {
 401                    new_update_task(
 402                        query,
 403                        multi_buffer_snapshot,
 404                        buffer_snapshot,
 405                        Arc::clone(&visible_hints),
 406                        cached_excerpt_hints,
 407                        previous_task,
 408                        cx,
 409                    )
 410                };
 411                match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
 412                    hash_map::Entry::Occupied(mut o) => {
 413                        let update_task = o.get_mut();
 414                        if update_task.is_running() {
 415                            match (update_task.invalidation_strategy(), invalidation_strategy) {
 416                                (InvalidationStrategy::Forced, _)
 417                                | (_, InvalidationStrategy::OnConflict) => {
 418                                    o.insert(UpdateTask::new(
 419                                        invalidation_strategy,
 420                                        new_update_task(None),
 421                                    ));
 422                                }
 423                                (_, InvalidationStrategy::Forced) => {
 424                                    if cache_is_empty {
 425                                        o.insert(UpdateTask::new(
 426                                            invalidation_strategy,
 427                                            new_update_task(None),
 428                                        ));
 429                                    } else if update_task.pending_refresh.is_none() {
 430                                        update_task.pending_refresh = Some(new_update_task(Some(
 431                                            update_task.current.1.is_running_rx.clone(),
 432                                        )));
 433                                    }
 434                                }
 435                                _ => {}
 436                            }
 437                        } else {
 438                            o.insert(UpdateTask::new(
 439                                invalidation_strategy,
 440                                new_update_task(None),
 441                            ));
 442                        }
 443                    }
 444                    hash_map::Entry::Vacant(v) => {
 445                        v.insert(UpdateTask::new(
 446                            invalidation_strategy,
 447                            new_update_task(None),
 448                        ));
 449                    }
 450                }
 451            }
 452        }
 453    }
 454}
 455
 456fn new_update_task(
 457    query: ExcerptQuery,
 458    multi_buffer_snapshot: MultiBufferSnapshot,
 459    buffer_snapshot: BufferSnapshot,
 460    visible_hints: Arc<Vec<Inlay>>,
 461    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
 462    task_before_refresh: Option<smol::channel::Receiver<()>>,
 463    cx: &mut ViewContext<'_, '_, Editor>,
 464) -> SpawnedTask {
 465    let hints_fetch_tasks = query.hints_fetch_ranges(&buffer_snapshot);
 466    let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
 467    let is_refresh_task = task_before_refresh.is_some();
 468    let _task = cx.spawn(|editor, cx| async move {
 469        let _is_running_tx = is_running_tx;
 470        if let Some(task_before_refresh) = task_before_refresh {
 471            task_before_refresh.recv().await.ok();
 472        }
 473        let create_update_task = |range| {
 474            fetch_and_update_hints(
 475                editor.clone(),
 476                multi_buffer_snapshot.clone(),
 477                buffer_snapshot.clone(),
 478                Arc::clone(&visible_hints),
 479                cached_excerpt_hints.as_ref().map(Arc::clone),
 480                query,
 481                range,
 482                cx.clone(),
 483            )
 484        };
 485
 486        if is_refresh_task {
 487            let visible_range_has_updates =
 488                match create_update_task(hints_fetch_tasks.visible_range).await {
 489                    Ok(updated) => updated,
 490                    Err(e) => {
 491                        error!("inlay hint visible range update task failed: {e:#}");
 492                        return;
 493                    }
 494                };
 495
 496            if visible_range_has_updates {
 497                let other_update_results = futures::future::join_all(
 498                    hints_fetch_tasks
 499                        .other_ranges
 500                        .into_iter()
 501                        .map(create_update_task),
 502                )
 503                .await;
 504
 505                for result in other_update_results {
 506                    if let Err(e) = result {
 507                        error!("inlay hint update task failed: {e:#}");
 508                        return;
 509                    }
 510                }
 511            }
 512        } else {
 513            let task_update_results = futures::future::join_all(
 514                std::iter::once(hints_fetch_tasks.visible_range)
 515                    .chain(hints_fetch_tasks.other_ranges.into_iter())
 516                    .map(create_update_task),
 517            )
 518            .await;
 519
 520            for result in task_update_results {
 521                if let Err(e) = result {
 522                    error!("inlay hint update task failed: {e:#}");
 523                }
 524            }
 525        }
 526    });
 527
 528    SpawnedTask {
 529        version: query.cache_version,
 530        _task,
 531        is_running_rx,
 532    }
 533}
 534
 535async fn fetch_and_update_hints(
 536    editor: gpui::WeakViewHandle<Editor>,
 537    multi_buffer_snapshot: MultiBufferSnapshot,
 538    buffer_snapshot: BufferSnapshot,
 539    visible_hints: Arc<Vec<Inlay>>,
 540    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
 541    query: ExcerptQuery,
 542    fetch_range: Range<language::Anchor>,
 543    mut cx: gpui::AsyncAppContext,
 544) -> anyhow::Result<bool> {
 545    let inlay_hints_fetch_task = editor
 546        .update(&mut cx, |editor, cx| {
 547            editor
 548                .buffer()
 549                .read(cx)
 550                .buffer(query.buffer_id)
 551                .and_then(|buffer| {
 552                    let project = editor.project.as_ref()?;
 553                    Some(project.update(cx, |project, cx| {
 554                        project.inlay_hints(buffer, fetch_range.clone(), cx)
 555                    }))
 556                })
 557        })
 558        .ok()
 559        .flatten();
 560    let mut update_happened = false;
 561    let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
 562
 563    let new_hints = inlay_hints_fetch_task
 564        .await
 565        .context("inlay hint fetch task")?;
 566    let background_task_buffer_snapshot = buffer_snapshot.clone();
 567    let backround_fetch_range = fetch_range.clone();
 568    if let Some(new_update) = cx
 569        .background()
 570        .spawn(async move {
 571            calculate_hint_updates(
 572                query,
 573                backround_fetch_range,
 574                new_hints,
 575                &background_task_buffer_snapshot,
 576                cached_excerpt_hints,
 577                &visible_hints,
 578            )
 579        })
 580        .await
 581    {
 582        update_happened = !new_update.add_to_cache.is_empty()
 583            || !new_update.remove_from_cache.is_empty()
 584            || !new_update.remove_from_visible.is_empty();
 585        editor
 586            .update(&mut cx, |editor, cx| {
 587                let cached_excerpt_hints = editor
 588                    .inlay_hint_cache
 589                    .hints
 590                    .entry(new_update.excerpt_id)
 591                    .or_insert_with(|| {
 592                        Arc::new(RwLock::new(CachedExcerptHints {
 593                            version: new_update.cache_version,
 594                            buffer_version: buffer_snapshot.version().clone(),
 595                            hints: Vec::new(),
 596                        }))
 597                    });
 598                let mut cached_excerpt_hints = cached_excerpt_hints.write();
 599                match new_update.cache_version.cmp(&cached_excerpt_hints.version) {
 600                    cmp::Ordering::Less => return,
 601                    cmp::Ordering::Greater | cmp::Ordering::Equal => {
 602                        cached_excerpt_hints.version = new_update.cache_version;
 603                    }
 604                }
 605                cached_excerpt_hints
 606                    .hints
 607                    .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
 608                cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
 609                editor.inlay_hint_cache.version += 1;
 610
 611                let mut splice = InlaySplice {
 612                    to_remove: new_update.remove_from_visible,
 613                    to_insert: Vec::new(),
 614                };
 615
 616                for new_hint in new_update.add_to_cache {
 617                    let new_hint_position = multi_buffer_snapshot
 618                        .anchor_in_excerpt(query.excerpt_id, new_hint.position);
 619                    let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id));
 620                    if editor
 621                        .inlay_hint_cache
 622                        .allowed_hint_kinds
 623                        .contains(&new_hint.kind)
 624                    {
 625                        splice
 626                            .to_insert
 627                            .push((new_hint_position, new_inlay_id, new_hint.clone()));
 628                    }
 629
 630                    cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
 631                }
 632
 633                cached_excerpt_hints
 634                    .hints
 635                    .sort_by(|(_, hint_a), (_, hint_b)| {
 636                        hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
 637                    });
 638                drop(cached_excerpt_hints);
 639
 640                let InlaySplice {
 641                    to_remove,
 642                    to_insert,
 643                } = splice;
 644                if !to_remove.is_empty() || !to_insert.is_empty() {
 645                    editor.splice_inlay_hints(to_remove, to_insert, cx)
 646                }
 647            })
 648            .ok();
 649    }
 650
 651    Ok(update_happened)
 652}
 653
 654fn calculate_hint_updates(
 655    query: ExcerptQuery,
 656    fetch_range: Range<language::Anchor>,
 657    new_excerpt_hints: Vec<InlayHint>,
 658    buffer_snapshot: &BufferSnapshot,
 659    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
 660    visible_hints: &[Inlay],
 661) -> Option<ExcerptHintsUpdate> {
 662    let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
 663    let mut excerpt_hints_to_persist = HashMap::default();
 664    for new_hint in new_excerpt_hints {
 665        if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
 666            continue;
 667        }
 668        let missing_from_cache = match &cached_excerpt_hints {
 669            Some(cached_excerpt_hints) => {
 670                let cached_excerpt_hints = cached_excerpt_hints.read();
 671                match cached_excerpt_hints.hints.binary_search_by(|probe| {
 672                    probe.1.position.cmp(&new_hint.position, buffer_snapshot)
 673                }) {
 674                    Ok(ix) => {
 675                        let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
 676                        if cached_hint == &new_hint {
 677                            excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
 678                            false
 679                        } else {
 680                            true
 681                        }
 682                    }
 683                    Err(_) => true,
 684                }
 685            }
 686            None => true,
 687        };
 688        if missing_from_cache {
 689            add_to_cache.insert(new_hint);
 690        }
 691    }
 692
 693    let mut remove_from_visible = Vec::new();
 694    let mut remove_from_cache = HashSet::default();
 695    if matches!(
 696        query.invalidate,
 697        InvalidationStrategy::Forced | InvalidationStrategy::OnConflict
 698    ) {
 699        remove_from_visible.extend(
 700            visible_hints
 701                .iter()
 702                .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
 703                .filter(|hint| {
 704                    contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
 705                })
 706                .filter(|hint| {
 707                    fetch_range
 708                        .start
 709                        .cmp(&hint.position.text_anchor, buffer_snapshot)
 710                        .is_le()
 711                        && fetch_range
 712                            .end
 713                            .cmp(&hint.position.text_anchor, buffer_snapshot)
 714                            .is_ge()
 715                })
 716                .map(|inlay_hint| inlay_hint.id)
 717                .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
 718        );
 719
 720        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
 721            let cached_excerpt_hints = cached_excerpt_hints.read();
 722            remove_from_cache.extend(
 723                cached_excerpt_hints
 724                    .hints
 725                    .iter()
 726                    .filter(|(cached_inlay_id, _)| {
 727                        !excerpt_hints_to_persist.contains_key(cached_inlay_id)
 728                    })
 729                    .filter(|(_, cached_hint)| {
 730                        fetch_range
 731                            .start
 732                            .cmp(&cached_hint.position, buffer_snapshot)
 733                            .is_le()
 734                            && fetch_range
 735                                .end
 736                                .cmp(&cached_hint.position, buffer_snapshot)
 737                                .is_ge()
 738                    })
 739                    .map(|(cached_inlay_id, _)| *cached_inlay_id),
 740            );
 741        }
 742    }
 743
 744    if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
 745        None
 746    } else {
 747        Some(ExcerptHintsUpdate {
 748            cache_version: query.cache_version,
 749            excerpt_id: query.excerpt_id,
 750            remove_from_visible,
 751            remove_from_cache,
 752            add_to_cache,
 753        })
 754    }
 755}
 756
 757fn allowed_hint_types(
 758    inlay_hint_settings: editor_settings::InlayHints,
 759) -> HashSet<Option<InlayHintKind>> {
 760    let mut new_allowed_hint_types = HashSet::default();
 761    if inlay_hint_settings.show_type_hints {
 762        new_allowed_hint_types.insert(Some(InlayHintKind::Type));
 763    }
 764    if inlay_hint_settings.show_parameter_hints {
 765        new_allowed_hint_types.insert(Some(InlayHintKind::Parameter));
 766    }
 767    if inlay_hint_settings.show_other_hints {
 768        new_allowed_hint_types.insert(None);
 769    }
 770    new_allowed_hint_types
 771}
 772
 773struct HintFetchRanges {
 774    visible_range: Range<language::Anchor>,
 775    other_ranges: Vec<Range<language::Anchor>>,
 776}
 777
 778fn contains_position(
 779    range: &Range<language::Anchor>,
 780    position: language::Anchor,
 781    buffer_snapshot: &BufferSnapshot,
 782) -> bool {
 783    range.start.cmp(&position, buffer_snapshot).is_le()
 784        && range.end.cmp(&position, buffer_snapshot).is_ge()
 785}
 786
 787#[cfg(test)]
 788mod tests {
 789    use std::sync::atomic::{AtomicU32, Ordering};
 790
 791    use crate::serde_json::json;
 792    use futures::StreamExt;
 793    use gpui::{TestAppContext, ViewHandle};
 794    use language::{
 795        language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
 796    };
 797    use lsp::FakeLanguageServer;
 798    use project::{FakeFs, Project};
 799    use settings::SettingsStore;
 800    use workspace::Workspace;
 801
 802    use crate::{editor_tests::update_test_settings, EditorSettings};
 803
 804    use super::*;
 805
 806    #[gpui::test]
 807    async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
 808        init_test(cx, |_| {});
 809        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
 810        let (file_with_hints, editor, fake_server) =
 811            prepare_test_objects(cx, &allowed_hint_kinds).await;
 812
 813        let lsp_request_count = Arc::new(AtomicU32::new(0));
 814        fake_server
 815            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
 816                let task_lsp_request_count = Arc::clone(&lsp_request_count);
 817                async move {
 818                    assert_eq!(
 819                        params.text_document.uri,
 820                        lsp::Url::from_file_path(file_with_hints).unwrap(),
 821                    );
 822                    let current_call_id =
 823                        Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
 824                    let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
 825                    for _ in 0..2 {
 826                        let mut i = current_call_id;
 827                        loop {
 828                            new_hints.push(lsp::InlayHint {
 829                                position: lsp::Position::new(0, i),
 830                                label: lsp::InlayHintLabel::String(i.to_string()),
 831                                kind: None,
 832                                text_edits: None,
 833                                tooltip: None,
 834                                padding_left: None,
 835                                padding_right: None,
 836                                data: None,
 837                            });
 838                            if i == 0 {
 839                                break;
 840                            }
 841                            i -= 1;
 842                        }
 843                    }
 844
 845                    Ok(Some(new_hints))
 846                }
 847            })
 848            .next()
 849            .await;
 850        cx.foreground().finish_waiting();
 851        cx.foreground().run_until_parked();
 852        let mut edits_made = 1;
 853        editor.update(cx, |editor, cx| {
 854            let expected_layers = vec!["0".to_string()];
 855            assert_eq!(
 856                expected_layers,
 857                cached_hint_labels(editor),
 858                "Should get its first hints when opening the editor"
 859            );
 860            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
 861            let inlay_cache = editor.inlay_hint_cache();
 862            assert_eq!(
 863                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
 864                "Cache should use editor settings to get the allowed hint kinds"
 865            );
 866            assert_eq!(
 867                inlay_cache.version, edits_made,
 868                "The editor update the cache version after every cache/view change"
 869            );
 870        });
 871
 872        editor.update(cx, |editor, cx| {
 873            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
 874            editor.handle_input("some change", cx);
 875            edits_made += 1;
 876        });
 877        cx.foreground().run_until_parked();
 878        editor.update(cx, |editor, cx| {
 879            let expected_layers = vec!["0".to_string(), "1".to_string()];
 880            assert_eq!(
 881                expected_layers,
 882                cached_hint_labels(editor),
 883                "Should get new hints after an edit"
 884            );
 885            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
 886            let inlay_cache = editor.inlay_hint_cache();
 887            assert_eq!(
 888                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
 889                "Cache should use editor settings to get the allowed hint kinds"
 890            );
 891            assert_eq!(
 892                inlay_cache.version, edits_made,
 893                "The editor update the cache version after every cache/view change"
 894            );
 895        });
 896
 897        fake_server
 898            .request::<lsp::request::InlayHintRefreshRequest>(())
 899            .await
 900            .expect("inlay refresh request failed");
 901        edits_made += 1;
 902        cx.foreground().run_until_parked();
 903        editor.update(cx, |editor, cx| {
 904            let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
 905            assert_eq!(
 906                expected_layers,
 907                cached_hint_labels(editor),
 908                "Should get new hints after hint refresh/ request"
 909            );
 910            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
 911            let inlay_cache = editor.inlay_hint_cache();
 912            assert_eq!(
 913                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
 914                "Cache should use editor settings to get the allowed hint kinds"
 915            );
 916            assert_eq!(
 917                inlay_cache.version, edits_made,
 918                "The editor update the cache version after every cache/view change"
 919            );
 920        });
 921    }
 922
 923    async fn prepare_test_objects(
 924        cx: &mut TestAppContext,
 925        allowed_hint_kinds: &HashSet<Option<InlayHintKind>>,
 926    ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
 927        cx.update(|cx| {
 928            cx.update_global(|store: &mut SettingsStore, cx| {
 929                store.update_user_settings::<EditorSettings>(cx, |settings| {
 930                    settings.inlay_hints = Some(crate::InlayHintsContent {
 931                        enabled: Some(true),
 932                        show_type_hints: Some(
 933                            allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
 934                        ),
 935                        show_parameter_hints: Some(
 936                            allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
 937                        ),
 938                        show_other_hints: Some(allowed_hint_kinds.contains(&None)),
 939                    })
 940                });
 941            });
 942        });
 943
 944        let mut language = Language::new(
 945            LanguageConfig {
 946                name: "Rust".into(),
 947                path_suffixes: vec!["rs".to_string()],
 948                ..Default::default()
 949            },
 950            Some(tree_sitter_rust::language()),
 951        );
 952        let mut fake_servers = language
 953            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 954                capabilities: lsp::ServerCapabilities {
 955                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
 956                    ..Default::default()
 957                },
 958                ..Default::default()
 959            }))
 960            .await;
 961
 962        let fs = FakeFs::new(cx.background());
 963        fs.insert_tree(
 964            "/a",
 965            json!({
 966                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
 967                "other.rs": "// Test file",
 968            }),
 969        )
 970        .await;
 971
 972        let project = Project::test(fs, ["/a".as_ref()], cx).await;
 973        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
 974        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 975        let worktree_id = workspace.update(cx, |workspace, cx| {
 976            workspace.project().read_with(cx, |project, cx| {
 977                project.worktrees(cx).next().unwrap().read(cx).id()
 978            })
 979        });
 980
 981        cx.foreground().start_waiting();
 982        let editor = workspace
 983            .update(cx, |workspace, cx| {
 984                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
 985            })
 986            .await
 987            .unwrap()
 988            .downcast::<Editor>()
 989            .unwrap();
 990
 991        let fake_server = fake_servers.next().await.unwrap();
 992
 993        ("/a/main.rs", editor, fake_server)
 994    }
 995
 996    #[gpui::test]
 997    async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
 998        init_test(cx, |_| {});
 999        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1000        let (file_with_hints, editor, fake_server) =
1001            prepare_test_objects(cx, &allowed_hint_kinds).await;
1002
1003        fake_server
1004            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| async move {
1005                assert_eq!(
1006                    params.text_document.uri,
1007                    lsp::Url::from_file_path(file_with_hints).unwrap(),
1008                );
1009                Ok(Some(vec![
1010                    lsp::InlayHint {
1011                        position: lsp::Position::new(0, 1),
1012                        label: lsp::InlayHintLabel::String("type hint".to_string()),
1013                        kind: Some(lsp::InlayHintKind::TYPE),
1014                        text_edits: None,
1015                        tooltip: None,
1016                        padding_left: None,
1017                        padding_right: None,
1018                        data: None,
1019                    },
1020                    lsp::InlayHint {
1021                        position: lsp::Position::new(0, 2),
1022                        label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1023                        kind: Some(lsp::InlayHintKind::PARAMETER),
1024                        text_edits: None,
1025                        tooltip: None,
1026                        padding_left: None,
1027                        padding_right: None,
1028                        data: None,
1029                    },
1030                    lsp::InlayHint {
1031                        position: lsp::Position::new(0, 3),
1032                        label: lsp::InlayHintLabel::String("other hint".to_string()),
1033                        kind: None,
1034                        text_edits: None,
1035                        tooltip: None,
1036                        padding_left: None,
1037                        padding_right: None,
1038                        data: None,
1039                    },
1040                ]))
1041            })
1042            .next()
1043            .await;
1044        cx.foreground().finish_waiting();
1045        cx.foreground().run_until_parked();
1046
1047        let edits_made = 1;
1048        editor.update(cx, |editor, cx| {
1049            assert_eq!(
1050                vec![
1051                    "type hint".to_string(),
1052                    "parameter hint".to_string(),
1053                    "other hint".to_string()
1054                ],
1055                cached_hint_labels(editor),
1056                "Should get its first hints when opening the editor"
1057            );
1058            assert_eq!(
1059                vec!["type hint".to_string(), "other hint".to_string()],
1060                visible_hint_labels(editor, cx)
1061            );
1062            let inlay_cache = editor.inlay_hint_cache();
1063            assert_eq!(
1064                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1065                "Cache should use editor settings to get the allowed hint kinds"
1066            );
1067            assert_eq!(
1068                inlay_cache.version, edits_made,
1069                "The editor update the cache version after every cache/view change"
1070            );
1071        });
1072
1073        //
1074    }
1075
1076    pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1077        cx.foreground().forbid_parking();
1078
1079        cx.update(|cx| {
1080            cx.set_global(SettingsStore::test(cx));
1081            theme::init((), cx);
1082            client::init_settings(cx);
1083            language::init(cx);
1084            Project::init_settings(cx);
1085            workspace::init_settings(cx);
1086            crate::init(cx);
1087        });
1088
1089        update_test_settings(cx, f);
1090    }
1091
1092    fn cached_hint_labels(editor: &Editor) -> Vec<String> {
1093        let mut labels = Vec::new();
1094        for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
1095            let excerpt_hints = excerpt_hints.read();
1096            for (_, inlay) in excerpt_hints.hints.iter() {
1097                match &inlay.label {
1098                    project::InlayHintLabel::String(s) => labels.push(s.to_string()),
1099                    _ => unreachable!(),
1100                }
1101            }
1102        }
1103        labels
1104    }
1105
1106    fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
1107        editor
1108            .visible_inlay_hints(cx)
1109            .into_iter()
1110            .map(|hint| hint.text.to_string())
1111            .collect()
1112    }
1113}