use std::rc::Rc;
use std::time::Duration;
use crate::lsp::{
CompletionPopupState, CompletionProvider, CompletionState, DiagnosticEntry,
DiagnosticsProvider, HoverProvider, HoverState, PositionMapping,
};
use crate::{App, Context, Entity, HighlightStyle, Hsla, Task, UnderlineStyle, Window, px};
use lsp_types::{
CompletionContext, CompletionResponse, CompletionTriggerKind, DiagnosticSeverity,
InsertTextFormat, Uri,
};
use super::super::decorations::{TextDecoration, normalize};
use super::state::EditorState;
use crate::input_ui::TextDecorationCollection;
const COMPLETION_DEBOUNCE: Duration = Duration::from_millis(150);
const DIAGNOSTICS_DEBOUNCE: Duration = Duration::from_millis(500);
const HOVER_DEBOUNCE: Duration = Duration::from_millis(200);
pub(super) struct LspAttach {
completion_provider: Option<Rc<dyn CompletionProvider>>,
diagnostics_provider: Option<Rc<dyn DiagnosticsProvider>>,
hover_provider: Option<Rc<dyn HoverProvider>>,
document_uri: Option<Uri>,
completion: CompletionState,
popup: Entity<CompletionPopupState>,
diagnostics: Vec<DiagnosticEntry>,
diagnostics_collection: Option<TextDecorationCollection>,
hover: HoverState,
auto_completion: bool,
auto_min_prefix_len: usize,
auto_suppress_once: bool,
epoch: u64,
_lsp_task: Option<Task<()>>,
}
impl LspAttach {
pub(super) fn new(popup: Entity<CompletionPopupState>) -> Self {
Self {
completion_provider: None,
diagnostics_provider: None,
hover_provider: None,
document_uri: None,
completion: CompletionState::default(),
popup,
diagnostics: Vec::new(),
diagnostics_collection: None,
hover: HoverState::default(),
auto_completion: false,
auto_min_prefix_len: 2,
auto_suppress_once: false,
epoch: 0,
_lsp_task: None,
}
}
fn next_epoch(&mut self) -> u64 {
self.epoch = self.epoch.wrapping_add(1);
self.epoch
}
}
fn is_completion_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn word_start_before(text: &ropey::Rope, cursor: usize) -> usize {
let mut start = 0;
let mut off = 0;
for ch in text.slice(..cursor).chars() {
off += ch.len_utf8();
if !is_completion_word_char(ch) {
start = off;
}
}
start
}
fn word_range_at(text: &ropey::Rope, cursor: usize) -> std::ops::Range<usize> {
let len = text.len();
let cursor = cursor.min(len);
let start = word_start_before(text, cursor);
let mut end = cursor;
for ch in text.slice(cursor..).chars() {
if !is_completion_word_char(ch) {
break;
}
end += ch.len_utf8();
}
start..end
}
fn diagnostic_color(severity: DiagnosticSeverity) -> Hsla {
match severity {
DiagnosticSeverity::ERROR => crate::red_400(),
DiagnosticSeverity::WARNING => crate::yellow_400(),
_ => crate::gray_400(),
}
}
fn diagnostic_wavy(severity: DiagnosticSeverity) -> bool {
severity == DiagnosticSeverity::ERROR
}
impl EditorState {
pub fn set_completion_provider(
&mut self,
provider: Option<Rc<dyn CompletionProvider>>,
cx: &mut Context<Self>,
) {
self.lsp.completion_provider = provider;
if self.lsp.completion_provider.is_none() {
self.dismiss_completion(cx);
}
}
pub fn set_auto_completion_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
self.lsp.auto_completion = enabled;
if !enabled {
self.dismiss_completion(cx);
} else {
cx.notify();
}
}
pub fn auto_completion_enabled(&self) -> bool {
self.lsp.auto_completion
}
pub fn set_auto_completion_min_prefix_len(&mut self, len: usize, cx: &mut Context<Self>) {
self.lsp.auto_min_prefix_len = len;
cx.notify();
}
pub fn auto_completion_min_prefix_len(&self) -> usize {
self.lsp.auto_min_prefix_len
}
pub(crate) fn maybe_auto_complete(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(provider) = self.lsp.completion_provider.clone() else {
return;
};
if !self.lsp.auto_completion {
return;
}
if self.lsp.auto_suppress_once {
self.lsp.auto_suppress_once = false;
return;
}
let (last_char, prefix_len) = self.input.read_with(cx, |state, _| {
let cursor = state.cursor().min(state.text().len());
let text = state.text();
let last = text.slice(..cursor).chars().last();
let prefix_len = cursor - word_start_before(text, cursor);
(last, prefix_len)
});
let is_trigger_char = last_char.is_some_and(|c| {
c == '.'
|| c == ':'
|| provider
.trigger_characters()
.iter()
.any(|t| *t == c.to_string())
});
let long_enough_word = last_char.is_some_and(is_completion_word_char)
&& prefix_len >= self.lsp.auto_min_prefix_len;
if is_trigger_char || long_enough_word {
self.request_completions(window, cx);
} else if self.lsp.completion.visible {
self.dismiss_completion(cx);
}
}
pub fn set_diagnostics_provider(
&mut self,
provider: Option<Rc<dyn DiagnosticsProvider>>,
cx: &mut Context<Self>,
) {
self.lsp.diagnostics_provider = provider;
if self.lsp.diagnostics_provider.is_none() {
self.lsp.next_epoch();
self.lsp.diagnostics.clear();
self.refresh_diagnostic_decorations(cx);
}
}
pub fn set_hover_provider(
&mut self,
provider: Option<Rc<dyn HoverProvider>>,
cx: &mut Context<Self>,
) {
self.lsp.hover_provider = provider;
if self.lsp.hover_provider.is_none() {
self.lsp.next_epoch();
self.lsp.hover.clear();
cx.notify();
}
}
pub fn set_document_uri(&mut self, uri: Option<Uri>, cx: &mut Context<Self>) {
self.lsp.document_uri = uri;
cx.notify();
}
pub fn completion_state(&self) -> &CompletionState {
&self.lsp.completion
}
pub fn completion_popup(&self) -> &Entity<CompletionPopupState> {
&self.lsp.popup
}
pub fn diagnostics(&self) -> &[DiagnosticEntry] {
&self.lsp.diagnostics
}
pub fn hover_state(&self) -> &HoverState {
&self.lsp.hover
}
pub fn request_completions(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(provider) = self.lsp.completion_provider.clone() else {
return;
};
let epoch = self.lsp.next_epoch();
let (text, offset) = self
.input
.read_with(cx, |state, _| (state.text().clone(), state.cursor()));
let trigger = CompletionContext {
trigger_kind: CompletionTriggerKind::INVOKED,
trigger_character: None,
};
self.lsp._lsp_task = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(COMPLETION_DEBOUNCE).await;
let task = this
.update_in(cx, |this, window, cx| {
if this.lsp.epoch != epoch {
return None;
}
Some(provider.completions(&text, offset, trigger, window, cx))
})
.ok()
.flatten();
let Some(task) = task else { return };
let response = task.await;
let _ = this.update_in(cx, |this, _, cx| {
if this.lsp.epoch != epoch {
return;
}
match response {
Ok(response) => this.apply_completion_response(response, cx),
Err(_) => this.dismiss_completion(cx),
}
});
}));
}
fn apply_completion_response(&mut self, response: CompletionResponse, cx: &mut Context<Self>) {
let max_items = self
.lsp
.completion_provider
.as_ref()
.map(|p| p.menu_options().max_visible_items)
.unwrap_or(15);
let items = CompletionState::from_response(response, max_items);
self.lsp.completion.completions = items;
self.lsp.completion.selected_index = 0;
self.lsp.completion.visible = !self.lsp.completion.completions.is_empty();
self.sync_completion_popup(cx);
cx.notify();
}
fn sync_completion_popup(&self, cx: &mut App) {
let popup = self.lsp.popup.clone();
let anchor = self.input.read(cx).cursor();
let anchor = self
.input
.read(cx)
.range_to_bounds(&(anchor..anchor))
.map(|bounds| bounds.bottom_left());
let completion = &self.lsp.completion;
let _ = popup.update(cx, |popup, cx| {
popup.update_from_state(completion);
if let Some(anchor) = anchor {
popup.position = anchor;
}
cx.notify();
});
}
pub fn accept_completion(
&mut self,
index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let index = index.unwrap_or(self.lsp.completion.selected_index);
let Some(item) = self.lsp.completion.completions.get(index).cloned() else {
return;
};
self.lsp.auto_suppress_once = true;
if item.lsp_item.insert_text_format == Some(InsertTextFormat::SNIPPET) {
self.expand_snippet(item.insert_text.as_str(), window, cx);
self.dismiss_completion(cx);
return;
}
let insert_text = item.insert_text.clone();
let word_range = self
.input
.read_with(cx, |state, _| word_range_at(state.text(), state.cursor()));
self.input.update(cx, |state, cx| {
state.set_selected_range(word_range, cx);
state.replace(insert_text.as_str(), window, cx);
});
self.dismiss_completion(cx);
}
pub fn dismiss_completion(&mut self, cx: &mut Context<Self>) {
self.lsp.completion.clear();
self.sync_completion_popup(cx);
cx.notify();
}
pub fn completion_menu_active(&self) -> bool {
self.lsp.completion.visible && self.lsp.completion.selected().is_some()
}
pub fn select_next_completion(&mut self, cx: &mut Context<Self>) {
if !self.lsp.completion.visible {
return;
}
self.lsp.completion.select_next();
self.sync_completion_popup(cx);
cx.notify();
}
pub fn select_previous_completion(&mut self, cx: &mut Context<Self>) {
if !self.lsp.completion.visible {
return;
}
self.lsp.completion.select_previous();
self.sync_completion_popup(cx);
cx.notify();
}
pub fn request_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(provider) = self.lsp.diagnostics_provider.clone() else {
return;
};
let Some(uri) = self.lsp.document_uri.clone() else {
return;
};
let epoch = self.lsp.next_epoch();
self.lsp._lsp_task = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(DIAGNOSTICS_DEBOUNCE).await;
let task = this
.update_in(cx, |this, window, cx| {
if this.lsp.epoch != epoch {
return None;
}
Some(provider.diagnostics(&uri, window, cx))
})
.ok()
.flatten();
let Some(task) = task else { return };
let response = task.await;
let _ = this.update_in(cx, |this, _, cx| {
if this.lsp.epoch != epoch {
return;
}
if let Ok(entries) = response {
this.lsp.diagnostics = entries;
this.refresh_diagnostic_decorations(cx);
}
});
}));
}
fn refresh_diagnostic_decorations(&mut self, cx: &mut Context<Self>) {
let diagnostics = self.lsp.diagnostics.clone();
let existing = self.lsp.diagnostics_collection.clone();
let mut created = None;
self.input.update(cx, |state, cx| {
let text = state.text().clone();
let decorations: Vec<TextDecoration> = diagnostics
.iter()
.map(|entry| {
let range = PositionMapping::position_to_offset(&text, entry.range.start)
..PositionMapping::position_to_offset(&text, entry.range.end);
TextDecoration::new(
range,
HighlightStyle {
underline: Some(UnderlineStyle {
thickness: px(1.),
color: Some(diagnostic_color(entry.severity)),
wavy: diagnostic_wavy(entry.severity),
}),
..Default::default()
},
)
})
.collect();
if let Some(collection) = existing {
let decorations = normalize(&text, decorations);
if collection.set_in_place(&mut state.core.decorations, decorations) {
cx.notify();
}
} else if !decorations.is_empty() {
created = Some(state.create_decorations_collection(decorations, cx));
}
});
if let Some(collection) = created {
self.lsp.diagnostics_collection = Some(collection);
}
}
pub fn request_hover(&mut self, offset: usize, window: &mut Window, cx: &mut Context<Self>) {
let Some(provider) = self.lsp.hover_provider.clone() else {
return;
};
let epoch = self.lsp.next_epoch();
let text = self.input.read_with(cx, |state, _| state.text().clone());
self.lsp._lsp_task = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(HOVER_DEBOUNCE).await;
let task = this
.update_in(cx, |this, window, cx| {
if this.lsp.epoch != epoch {
return None;
}
Some(provider.hover(&text, offset, window, cx))
})
.ok()
.flatten();
let Some(task) = task else { return };
let response = task.await;
let _ = this.update_in(cx, |this, _, cx| {
if this.lsp.epoch != epoch {
return;
}
match response {
Ok(response) => {
this.lsp.hover.response = response;
this.lsp.hover.offset = Some(offset);
this.lsp.hover.visible = this.lsp.hover.response.is_some();
cx.notify();
}
Err(_) => this.lsp.hover.clear(),
}
});
}));
}
pub fn dismiss_hover(&mut self, cx: &mut Context<Self>) {
self.lsp.hover.clear();
cx.notify();
}
}
#[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 FakeCompletionProvider;
impl CompletionProvider for FakeCompletionProvider {
fn completions(
&self,
_text: &ropey::Rope,
_offset: usize,
_trigger: CompletionContext,
_window: &mut Window,
_cx: &mut App,
) -> Task<anyhow::Result<CompletionResponse>> {
Task::ready(Ok(CompletionResponse::Array(vec![
lsp_types::CompletionItem {
label: "println".to_string(),
..Default::default()
},
lsp_types::CompletionItem {
label: "print".to_string(),
..Default::default()
},
])))
}
}
struct FakeDiagnosticsProvider;
impl DiagnosticsProvider for FakeDiagnosticsProvider {
fn diagnostics(
&self,
_uri: &Uri,
_window: &mut Window,
_cx: &mut App,
) -> Task<anyhow::Result<Vec<DiagnosticEntry>>> {
use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
Task::ready(Ok(vec![DiagnosticEntry::from_diagnostic(Diagnostic {
range: Range {
start: Position::new(0, 0),
end: Position::new(0, 5),
},
severity: Some(DiagnosticSeverity::ERROR),
message: "fake error".to_string(),
..Default::default()
})]))
}
}
struct FakeHoverProvider;
impl HoverProvider for FakeHoverProvider {
fn hover(
&self,
_text: &ropey::Rope,
offset: usize,
_window: &mut Window,
_cx: &mut App,
) -> Task<anyhow::Result<Option<crate::lsp::HoverResponse>>> {
Task::ready(Ok(Some(crate::lsp::HoverResponse {
range: offset..offset,
contents: vec![crate::lsp::HoverContent::Text("fn main".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();
}
fn test_uri() -> Uri {
"file:///test.rs".parse().unwrap()
}
#[rgpui::test]
fn completion_request_fills_popup(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_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.request_completions(window, cx);
});
});
pump(cx);
let visible = editor.read_with(cx, |state, _| state.completion_state().visible);
assert!(visible);
let labels: Vec<String> = editor.read_with(cx, |state, _| {
state
.completion_state()
.completions
.iter()
.map(|c| c.label.clone())
.collect()
});
assert_eq!(labels, vec!["println".to_string(), "print".to_string()]);
let popup_visible =
editor.read_with(cx, |state, cx| state.completion_popup().read(cx).visible);
assert!(popup_visible);
}
#[rgpui::test]
fn accept_completion_inserts_text(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn "));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.request_completions(window, cx);
});
});
pump(cx);
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.accept_completion(None, window, cx);
});
});
assert_eq!(
editor.read_with(cx, |state, cx| state.text(cx)),
"fn println"
);
assert!(!editor.read_with(cx, |state, _| state.completion_state().visible));
}
#[rgpui::test]
fn diagnostics_request_renders_underline(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_diagnostics_provider(Some(Rc::new(FakeDiagnosticsProvider)), cx);
state.set_document_uri(Some(test_uri()), cx);
state.request_diagnostics(window, cx);
});
});
pump(cx);
let count = editor.read_with(cx, |state, _| state.diagnostics().len());
assert_eq!(count, 1);
assert_eq!(
editor.read_with(cx, |state, _| state.diagnostics()[0].message.clone()),
"fake error"
);
assert!(editor.read_with(cx, |state, _| state.lsp.diagnostics_collection.is_some()));
}
#[rgpui::test]
fn hover_request_fills_state(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_hover_provider(Some(Rc::new(FakeHoverProvider)), cx);
state.request_hover(0, window, cx);
});
});
pump(cx);
let (visible, text) = editor.read_with(cx, |state, _| {
let hover = state.hover_state();
let text = hover
.response
.as_ref()
.map(|r| format!("{:?}", r.contents))
.unwrap_or_default();
(hover.visible, text)
});
assert!(visible);
assert!(text.contains("fn main"));
}
fn type_text(editor: &Entity<EditorState>, text: &str, cx: &mut crate::VisualTestContext) {
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
let input = state.input().clone();
input.update(cx, |state, cx| {
crate::EntityInputHandler::replace_text_in_range(state, None, text, window, cx);
});
});
});
}
fn completion_visible(editor: &Entity<EditorState>, cx: &mut crate::VisualTestContext) -> bool {
editor.read_with(cx, |state, _| state.completion_state().visible)
}
fn selected_index(editor: &Entity<EditorState>, cx: &mut crate::VisualTestContext) -> usize {
editor.read_with(cx, |state, _| state.completion_state().selected_index)
}
#[rgpui::test]
fn auto_completion_off_by_default(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn "));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
assert!(!editor.read_with(cx, |state, _| state.auto_completion_enabled()));
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
});
});
type_text(&editor, "p", cx);
pump(cx);
assert!(!completion_visible(&editor, cx));
}
#[rgpui::test]
fn auto_completion_triggers_on_word_input(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn "));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.set_auto_completion_enabled(true, cx);
});
});
assert!(editor.read_with(cx, |state, _| state.auto_completion_enabled()));
type_text(&editor, "p", cx);
pump(cx);
assert!(!completion_visible(&editor, cx));
type_text(&editor, "r", cx);
pump(cx);
assert!(completion_visible(&editor, cx));
type_text(&editor, " ", cx);
pump(cx);
assert!(!completion_visible(&editor, cx));
}
#[rgpui::test]
fn auto_completion_trigger_char_ignores_threshold(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn "));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.set_auto_completion_enabled(true, cx);
});
});
type_text(&editor, ".", cx);
pump(cx);
assert!(completion_visible(&editor, cx));
}
#[rgpui::test]
fn auto_completion_min_prefix_len_configurable(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn "));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
assert_eq!(
editor.read_with(cx, |state, _| state.auto_completion_min_prefix_len()),
2
);
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.set_auto_completion_enabled(true, cx);
state.set_auto_completion_min_prefix_len(1, cx);
});
});
type_text(&editor, "p", cx);
pump(cx);
assert!(completion_visible(&editor, cx));
}
#[rgpui::test]
fn accept_completion_suppresses_auto_retrigger(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn "));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.set_auto_completion_enabled(true, cx);
});
});
type_text(&editor, "pr", cx);
pump(cx);
assert!(completion_visible(&editor, cx));
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.accept_completion(None, window, cx);
});
});
pump(cx);
assert!(!completion_visible(&editor, cx));
type_text(&editor, "x", cx);
pump(cx);
assert!(completion_visible(&editor, cx));
}
#[rgpui::test]
fn disabling_auto_completion_dismisses_popup(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn "));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.request_completions(window, cx);
});
});
pump(cx);
assert!(completion_visible(&editor, cx));
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.set_auto_completion_enabled(false, cx);
});
});
assert!(!completion_visible(&editor, cx));
}
#[rgpui::test]
fn accept_completion_replaces_word_prefix(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn pr"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.request_completions(window, cx);
});
});
pump(cx);
assert!(completion_visible(&editor, cx));
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.accept_completion(Some(0), window, cx);
});
});
assert_eq!(
editor.read_with(cx, |state, cx| state.text(cx)),
"fn println"
);
assert!(!completion_visible(&editor, cx));
}
#[rgpui::test]
fn accept_completion_replaces_word_suffix(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "prln"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.request_completions(window, cx);
});
});
pump(cx);
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.set_selected_range(2..2, cx);
});
});
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.accept_completion(Some(0), window, cx);
});
});
assert_eq!(editor.read_with(cx, |state, cx| state.text(cx)), "println");
}
#[rgpui::test]
fn completion_selection_moves_and_wraps(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn "));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.select_next_completion(cx);
state.select_previous_completion(cx);
});
});
assert!(!completion_visible(&editor, cx));
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_completion_provider(Some(Rc::new(FakeCompletionProvider)), cx);
state.request_completions(window, cx);
});
});
pump(cx);
assert!(editor.read_with(cx, |state, _| state.completion_menu_active()));
assert_eq!(selected_index(&editor, cx), 0);
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.select_next_completion(cx);
});
});
assert_eq!(selected_index(&editor, cx), 1);
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.select_next_completion(cx);
});
});
assert_eq!(selected_index(&editor, cx), 0);
cx.update(|_, cx| {
editor.update(cx, |state, cx| {
state.select_previous_completion(cx);
});
});
assert_eq!(selected_index(&editor, cx), 1);
}
}