use std::ops::Range;
use std::rc::Rc;
use std::time::Duration;
use ropey::Rope;
use crate::{App, Context, Task, Window};
use super::state::EditorState;
const INLAY_DEBOUNCE: Duration = Duration::from_millis(300);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlayHint {
pub offset: usize,
pub text: String,
}
pub trait InlayProvider {
fn inlay_hints(
&self,
text: &Rope,
visible: Range<usize>,
window: &mut Window,
cx: &mut App,
) -> Task<anyhow::Result<Vec<InlayHint>>> {
let _ = (text, visible, window, cx);
Task::ready(Ok(Vec::new()))
}
}
pub(super) struct InlayState {
provider: Option<Rc<dyn InlayProvider>>,
epoch: u64,
_task: Option<Task<()>>,
}
impl InlayState {
pub(super) fn new() -> Self {
Self {
provider: None,
epoch: 0,
_task: None,
}
}
fn next_epoch(&mut self) -> u64 {
self.epoch = self.epoch.wrapping_add(1);
self.epoch
}
}
impl EditorState {
pub fn set_inlay_provider(
&mut self,
provider: Option<Rc<dyn InlayProvider>>,
cx: &mut Context<Self>,
) {
self.inlay.provider = provider;
if self.inlay.provider.is_none() {
self.inlay.next_epoch();
self.clear_inlay_hints(cx);
}
}
pub fn set_inlay_hints_enabled(&self, enabled: bool, cx: &mut App) {
let _ = self.input.update(cx, |state, cx| {
state.inlay_hints_enabled = enabled;
if !enabled {
state.inlay_hints.clear();
}
cx.notify();
});
}
pub fn inlay_hints_enabled(&self, cx: &App) -> bool {
self.input
.read_with(cx, |state, _| state.inlay_hints_enabled)
}
pub fn inlay_hint_list(&self, cx: &App) -> Vec<InlayHint> {
self.input
.read_with(cx, |state, _| state.inlay_hints.clone())
}
pub fn request_inlay_hints(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(provider) = self.inlay.provider.clone() else {
return;
};
let (text, visible, enabled) = self.input.read_with(cx, |state, _| {
let visible = state
.last_layout
.as_ref()
.map(|layout| layout.visible_range_offset.clone())
.unwrap_or(0..state.text().len());
(state.text().clone(), visible, state.inlay_hints_enabled)
});
if !enabled {
return;
}
let epoch = self.inlay.next_epoch();
self.inlay._task = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(INLAY_DEBOUNCE).await;
let task = this
.update_in(cx, |this, window, cx| {
if this.inlay.epoch != epoch {
return None;
}
Some(provider.inlay_hints(&text, visible, window, cx))
})
.ok()
.flatten();
let Some(task) = task else { return };
let response = task.await;
let _ = this.update_in(cx, |this, _, cx| {
if this.inlay.epoch != epoch {
return;
}
if let Ok(hints) = response {
this.input.update(cx, |state, cx| {
state.inlay_hints = hints;
cx.notify();
});
}
});
}));
}
fn clear_inlay_hints(&mut self, cx: &mut Context<Self>) {
self.input.update(cx, |state, cx| {
state.inlay_hints.clear();
cx.notify();
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AppContext as _;
use crate::{Entity, Render};
struct Probe {
state: Entity<EditorState>,
}
impl Render for Probe {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> impl crate::IntoElement {
crate::div()
}
}
struct FakeInlayProvider;
impl InlayProvider for FakeInlayProvider {
fn inlay_hints(
&self,
_text: &Rope,
visible: Range<usize>,
_window: &mut Window,
_cx: &mut App,
) -> Task<anyhow::Result<Vec<InlayHint>>> {
assert!(!visible.is_empty());
Task::ready(Ok(vec![
InlayHint {
offset: 2,
text: ": i32".to_string(),
},
InlayHint {
offset: 5,
text: "-> ()".to_string(),
},
]))
}
}
fn pump(cx: &mut crate::TestAppContext) {
cx.dispatcher.advance_clock(Duration::from_millis(2000));
cx.run_until_parked();
cx.background_executor.run_until_parked();
cx.run_until_parked();
}
#[rgpui::test]
fn inlay_request_stores_hints(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
assert!(!editor.read_with(cx, |state, cx| state.inlay_hints_enabled(cx)));
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_inlay_provider(Some(Rc::new(FakeInlayProvider)), cx);
state.set_inlay_hints_enabled(true, cx);
state.request_inlay_hints(window, cx);
});
});
pump(cx);
let hints = editor.read_with(cx, |state, cx| state.inlay_hint_list(cx));
assert_eq!(hints.len(), 2);
assert_eq!(hints[0].offset, 2);
assert_eq!(hints[0].text, ": i32");
}
#[rgpui::test]
fn disabled_skips_request_and_clears(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_inlay_provider(Some(Rc::new(FakeInlayProvider)), cx);
state.request_inlay_hints(window, cx);
});
});
pump(cx);
assert!(
editor
.read_with(cx, |state, cx| state.inlay_hint_list(cx))
.is_empty()
);
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_inlay_hints_enabled(true, cx);
state.request_inlay_hints(window, cx);
});
});
pump(cx);
assert_eq!(
editor
.read_with(cx, |state, cx| state.inlay_hint_list(cx))
.len(),
2
);
editor.update(cx, |state, cx| {
state.set_inlay_hints_enabled(false, cx);
});
assert!(
editor
.read_with(cx, |state, cx| state.inlay_hint_list(cx))
.is_empty()
);
}
#[rgpui::test]
fn disconnect_clears_hints(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_inlay_provider(Some(Rc::new(FakeInlayProvider)), cx);
state.set_inlay_hints_enabled(true, cx);
state.request_inlay_hints(window, cx);
});
});
pump(cx);
assert_eq!(
editor
.read_with(cx, |state, cx| state.inlay_hint_list(cx))
.len(),
2
);
editor.update(cx, |state, cx| {
state.set_inlay_provider(None, cx);
});
assert!(
editor
.read_with(cx, |state, cx| state.inlay_hint_list(cx))
.is_empty()
);
}
}