use std::ops::Range;
use std::rc::Rc;
use std::time::Duration;
use ropey::{LineType, Rope};
use crate::{
ActiveTheme as _, Anchor, App, Context, Entity, InteractiveElement, IntoElement, ParentElement,
RenderOnce, StatefulInteractiveElement, Styled, Task, Window, anchored, deferred, div, px,
};
use super::state::EditorState;
const CODELENS_DEBOUNCE: Duration = Duration::from_millis(300);
const MAX_VISIBLE_LENSES: usize = 20;
#[derive(Debug, Clone)]
pub struct CodeLens {
pub line: u32,
pub title: crate::SharedString,
}
#[derive(Debug, Clone)]
pub struct ResolvedCodeLens {
pub line: u32,
pub offset: usize,
pub title: crate::SharedString,
}
pub trait CodeLensProvider {
fn codelenses(
&self,
text: &Rope,
visible: Range<usize>,
window: &mut Window,
cx: &mut App,
) -> Task<anyhow::Result<Vec<CodeLens>>> {
let _ = (text, visible, window, cx);
Task::ready(Ok(Vec::new()))
}
}
pub(super) struct CodelensState {
provider: Option<Rc<dyn CodeLensProvider>>,
lenses: Vec<ResolvedCodeLens>,
epoch: u64,
_task: Option<Task<()>>,
}
impl CodelensState {
pub(super) fn new() -> Self {
Self {
provider: None,
lenses: Vec::new(),
epoch: 0,
_task: None,
}
}
fn next_epoch(&mut self) -> u64 {
self.epoch = self.epoch.wrapping_add(1);
self.epoch
}
}
impl EditorState {
pub fn set_codelens_provider(
&mut self,
provider: Option<Rc<dyn CodeLensProvider>>,
cx: &mut Context<Self>,
) {
self.codelens.provider = provider;
if self.codelens.provider.is_none() {
self.codelens.next_epoch();
self.codelens.lenses.clear();
cx.notify();
}
}
pub fn codelenses(&self) -> &[ResolvedCodeLens] {
&self.codelens.lenses
}
pub fn request_codelenses(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(provider) = self.codelens.provider.clone() else {
return;
};
let (text, visible) = 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)
});
let epoch = self.codelens.next_epoch();
self.codelens._task = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(CODELENS_DEBOUNCE).await;
let task = this
.update_in(cx, |this, window, cx| {
if this.codelens.epoch != epoch {
return None;
}
Some(provider.codelenses(&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.codelens.epoch != epoch {
return;
}
if let Ok(lenses) = response {
let current = this.input.read_with(cx, |state, _| state.text().clone());
this.codelens.lenses = Self::resolve_lenses(¤t, lenses);
cx.notify();
}
});
}));
}
fn resolve_lenses(text: &Rope, lenses: Vec<CodeLens>) -> Vec<ResolvedCodeLens> {
let last_row = text.len_lines(LineType::LF).saturating_sub(1);
let mut resolved: Vec<ResolvedCodeLens> = lenses
.into_iter()
.map(|lens| {
let row = (lens.line as usize).min(last_row);
let offset = text.line_to_byte_idx(row, LineType::LF).min(text.len());
ResolvedCodeLens {
line: row as u32,
offset,
title: lens.title,
}
})
.collect();
resolved.sort_by_key(|lens| (lens.line, lens.offset));
resolved
}
}
#[derive(IntoElement)]
pub struct CodeLensOverlay {
editor: Entity<EditorState>,
on_lens: Option<Rc<dyn Fn(usize, &mut Window, &mut App)>>,
}
impl CodeLensOverlay {
pub fn new(editor: &Entity<EditorState>) -> Self {
Self {
editor: editor.clone(),
on_lens: None,
}
}
pub fn on_lens(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
self.on_lens = Some(Rc::new(handler));
self
}
}
impl RenderOnce for CodeLensOverlay {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (lenses, anchors) = self.editor.read_with(cx, |state, cx| {
let lenses: Vec<ResolvedCodeLens> = state
.codelenses()
.iter()
.take(MAX_VISIBLE_LENSES)
.cloned()
.collect();
let anchors: Vec<Option<crate::Point<crate::Pixels>>> = lenses
.iter()
.map(|lens| {
state
.input()
.read(cx)
.range_to_bounds(&(lens.offset..lens.offset))
.map(|bounds| bounds.origin - crate::point(px(0.), px(18.)))
})
.collect();
(lenses, anchors)
});
let on_lens = self.on_lens.clone();
div().children(lenses.into_iter().zip(anchors).enumerate().filter_map(
move |(ix, (lens, anchor))| {
let anchor = anchor?;
let on_lens = on_lens.clone();
let row = div()
.id(("codelens", ix))
.px_1()
.text_xs()
.text_color(cx.theme().muted_foreground)
.max_w(px(400.))
.overflow_hidden()
.whitespace_nowrap()
.hover(|this| this.text_color(cx.theme().foreground))
.child(lens.title);
let row = if let Some(on_lens) = on_lens {
row.cursor_pointer().on_click(move |_, window, cx| {
on_lens(ix, window, cx);
})
} else {
row
};
Some(
deferred(
anchored()
.position(anchor)
.anchor(Anchor::BottomLeft)
.snap_to_window_with_margin(px(8.))
.child(row),
)
.with_priority(1)
.into_any_element(),
)
},
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AppContext as _;
use crate::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 FakeCodelensProvider;
impl CodeLensProvider for FakeCodelensProvider {
fn codelenses(
&self,
_text: &Rope,
_visible: Range<usize>,
_window: &mut Window,
_cx: &mut App,
) -> Task<anyhow::Result<Vec<CodeLens>>> {
Task::ready(Ok(vec![
CodeLens {
line: 0,
title: "▶ 运行".into(),
},
CodeLens {
line: 5,
title: "3 引用".into(),
},
]))
}
}
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 codelens_request_resolves(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\nline2\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
assert!(editor.read_with(cx, |state, _| state.codelenses().is_empty()));
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_codelens_provider(Some(Rc::new(FakeCodelensProvider)), cx);
state.request_codelenses(window, cx);
});
});
pump(cx);
let lenses = editor.read_with(cx, |state, _| state.codelenses().to_vec());
assert_eq!(lenses.len(), 2);
assert_eq!(lenses[0].line, 0);
assert_eq!(lenses[0].offset, 0);
assert_eq!(lenses[0].title.to_string(), "▶ 运行");
assert_eq!(lenses[1].line, 2);
}
#[rgpui::test]
fn codelens_disconnect_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_codelens_provider(Some(Rc::new(FakeCodelensProvider)), cx);
state.request_codelenses(window, cx);
});
});
pump(cx);
assert!(!editor.read_with(cx, |state, _| state.codelenses().is_empty()));
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.set_codelens_provider(None, cx);
});
});
assert!(editor.read_with(cx, |state, _| state.codelenses().is_empty()));
}
}