diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 553da88cf1..695ffbeed3 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -50,6 +50,27 @@ pub fn init(http: Arc, node_runtime: Arc, cx: &mut }); cx.set_global(copilot.clone()); + cx.observe(&copilot, |handle, cx| { + let status = handle.read(cx).status(); + cx.update_global::( + move |filter, _cx| match status { + Status::Disabled => { + filter.filtered_namespaces.insert(COPILOT_NAMESPACE); + filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE); + } + Status::Authorized => { + filter.filtered_namespaces.remove(COPILOT_NAMESPACE); + filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + } + _ => { + filter.filtered_namespaces.insert(COPILOT_NAMESPACE); + filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + } + }, + ); + }) + .detach(); + sign_in::init(cx); cx.add_global_action(|_: &SignIn, cx| { if let Some(copilot) = Copilot::global(cx) { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f28c82407a..5058a4353f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1037,6 +1037,11 @@ impl CopilotState { let completion = self.completions.get(self.active_completion_index)?; let excerpt_id = self.excerpt_id?; let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?; + if !completion.range.start.is_valid(completion_buffer) + || !completion.range.end.is_valid(completion_buffer) + { + return None; + } let mut completion_range = completion.range.to_offset(&completion_buffer); let prefix_len = Self::common_prefix( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6220ad3354..4df29b1ed5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6098,6 +6098,65 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC }); } +#[gpui::test] +async fn test_copilot_completion_invalidation( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + one + twˇ + three + "}); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "two.foo()".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), + ..Default::default() + }], + vec![], + ); + cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\ntw\nthree\n"); + + editor.backspace(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\nt\nthree\n"); + + editor.backspace(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\n\nthree\n"); + + // Deleting across the original suggestion range invalidates it. + editor.backspace(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\nthree\n"); + assert_eq!(editor.text(cx), "one\nthree\n"); + }); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 68ec00056a..b5f4fb24ec 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -1,4 +1,7 @@ -use crate::{BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint, ToPointUtf16}; +use crate::{ + locator::Locator, BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint, + ToPointUtf16, +}; use anyhow::Result; use std::{cmp::Ordering, fmt::Debug, ops::Range}; use sum_tree::Bias; @@ -86,6 +89,20 @@ impl Anchor { { content.summary_for_anchor(self) } + + /// Returns true when the [Anchor] is located inside a visible fragment. + pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool { + if *self == Anchor::MIN || *self == Anchor::MAX { + true + } else { + let fragment_id = buffer.fragment_id_for_anchor(self); + let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(); + fragment_cursor.seek(&Some(fragment_id), Bias::Left, &None); + fragment_cursor + .item() + .map_or(false, |fragment| fragment.visible) + } + } } pub trait OffsetRangeExt {