inlay_hint_cache.rs

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