Start integrating Copilot with editor

Antonio Scandurra created

There's still a bit to do in terms of reusing the previous suggestion
when the prefix matches, but we're getting there.

Change summary

Cargo.lock                             |   1 
crates/copilot/src/copilot.rs          |  26 +++-
crates/copilot/src/sign_in.rs          |   2 
crates/editor/Cargo.toml               |   6 
crates/editor/src/editor.rs            | 159 ++++++++++++++++++++++++++++
crates/gpui/src/platform/mac/window.rs |   1 
6 files changed, 183 insertions(+), 12 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1951,6 +1951,7 @@ dependencies = [
  "clock",
  "collections",
  "context_menu",
+ "copilot",
  "ctor",
  "db",
  "drag_and_drop",

crates/copilot/src/copilot.rs 🔗

@@ -25,13 +25,13 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
     let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx));
     cx.set_global(copilot.clone());
     cx.add_global_action(|_: &SignIn, cx| {
-        let copilot = Copilot::global(cx);
+        let copilot = Copilot::global(cx).unwrap();
         copilot
             .update(cx, |copilot, cx| copilot.sign_in(cx))
             .detach_and_log_err(cx);
     });
     cx.add_global_action(|_: &SignOut, cx| {
-        let copilot = Copilot::global(cx);
+        let copilot = Copilot::global(cx).unwrap();
         copilot
             .update(cx, |copilot, cx| copilot.sign_out(cx))
             .detach_and_log_err(cx);
@@ -75,13 +75,19 @@ pub enum Status {
     Authorized,
 }
 
+impl Status {
+    pub fn is_authorized(&self) -> bool {
+        matches!(self, Status::Authorized)
+    }
+}
+
 #[derive(Debug)]
 pub struct Completion {
     pub position: Anchor,
     pub text: String,
 }
 
-struct Copilot {
+pub struct Copilot {
     server: CopilotServer,
 }
 
@@ -90,8 +96,12 @@ impl Entity for Copilot {
 }
 
 impl Copilot {
-    fn global(cx: &AppContext) -> ModelHandle<Self> {
-        cx.global::<ModelHandle<Self>>().clone()
+    pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
+        if cx.has_global::<ModelHandle<Self>>() {
+            Some(cx.global::<ModelHandle<Self>>().clone())
+        } else {
+            None
+        }
     }
 
     fn start(http: Arc<dyn HttpClient>, cx: &mut ModelContext<Self>) -> Self {
@@ -240,7 +250,7 @@ impl Copilot {
     where
         T: ToPointUtf16,
     {
-        let server = match self.authenticated_server() {
+        let server = match self.authorized_server() {
             Ok(server) => server,
             Err(error) => return Task::ready(Err(error)),
         };
@@ -268,7 +278,7 @@ impl Copilot {
     where
         T: ToPointUtf16,
     {
-        let server = match self.authenticated_server() {
+        let server = match self.authorized_server() {
             Ok(server) => server,
             Err(error) => return Task::ready(Err(error)),
         };
@@ -322,7 +332,7 @@ impl Copilot {
         }
     }
 
-    fn authenticated_server(&self) -> Result<Arc<LanguageServer>> {
+    fn authorized_server(&self) -> Result<Arc<LanguageServer>> {
         match &self.server {
             CopilotServer::Downloading => Err(anyhow!("copilot is still downloading")),
             CopilotServer::Error(error) => Err(anyhow!(

crates/copilot/src/sign_in.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
 use settings::Settings;
 
 pub fn init(cx: &mut MutableAppContext) {
-    let copilot = Copilot::global(cx);
+    let copilot = Copilot::global(cx).unwrap();
 
     let mut code_verification_window_id = None;
     cx.observe(&copilot, move |copilot, cx| {

crates/editor/Cargo.toml 🔗

@@ -22,10 +22,10 @@ test-support = [
 ]
 
 [dependencies]
-drag_and_drop = { path = "../drag_and_drop" }
-text = { path = "../text" }
 clock = { path = "../clock" }
+copilot = { path = "../copilot" }
 db = { path = "../db" }
+drag_and_drop = { path = "../drag_and_drop" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
 fuzzy = { path = "../fuzzy" }
@@ -38,10 +38,12 @@ rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
+text = { path = "../text" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 sqlez = { path = "../sqlez" }
 workspace = { path = "../workspace" }
+
 aho-corasick = "0.7"
 anyhow = "1.0"
 futures = "0.3"

crates/editor/src/editor.rs 🔗

@@ -24,6 +24,7 @@ use anyhow::Result;
 use blink_manager::BlinkManager;
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
+use copilot::Copilot;
 pub use display_map::DisplayPoint;
 use display_map::*;
 pub use element::*;
@@ -96,6 +97,7 @@ const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
 const MAX_SELECTION_HISTORY_LEN: usize = 1024;
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
+pub const COPILOT_TIMEOUT: Duration = Duration::from_secs(1);
 
 #[derive(Clone, Deserialize, PartialEq, Default)]
 pub struct SelectNext {
@@ -260,6 +262,7 @@ actions!(
         ToggleSoftWrap,
         RevealInFinder,
         CopyHighlightJson
+        CycleCopilotSuggestions
     ]
 );
 
@@ -388,6 +391,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_async_action(Editor::rename);
     cx.add_async_action(Editor::confirm_rename);
     cx.add_async_action(Editor::find_all_references);
+    cx.add_action(Editor::cycle_copilot_suggestions);
 
     hover_popover::init(cx);
     link_go_to_definition::init(cx);
@@ -506,6 +510,7 @@ pub struct Editor {
     hover_state: HoverState,
     gutter_hovered: bool,
     link_go_to_definition_state: LinkGoToDefinitionState,
+    copilot_state: CopilotState,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -1003,6 +1008,30 @@ impl CodeActionsMenu {
     }
 }
 
+struct CopilotState {
+    position: Anchor,
+    pending_refresh: Task<Option<()>>,
+    completions: Vec<copilot::Completion>,
+    active_completion_index: usize,
+}
+
+impl Default for CopilotState {
+    fn default() -> Self {
+        Self {
+            position: Anchor::min(),
+            pending_refresh: Task::ready(Some(())),
+            completions: Default::default(),
+            active_completion_index: 0,
+        }
+    }
+}
+
+impl CopilotState {
+    fn active_completion(&self) -> Option<&copilot::Completion> {
+        self.completions.get(self.active_completion_index)
+    }
+}
+
 #[derive(Debug)]
 struct ActiveDiagnosticGroup {
     primary_range: Range<Anchor>,
@@ -1176,6 +1205,7 @@ impl Editor {
             remote_id: None,
             hover_state: Default::default(),
             link_go_to_definition_state: Default::default(),
+            copilot_state: Default::default(),
             gutter_hovered: false,
             _subscriptions: vec![
                 cx.observe(&buffer, Self::on_buffer_changed),
@@ -1385,6 +1415,7 @@ impl Editor {
             self.refresh_code_actions(cx);
             self.refresh_document_highlights(cx);
             refresh_matching_bracket_highlights(self, cx);
+            self.refresh_copilot_suggestions(cx);
         }
 
         self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -2677,6 +2708,129 @@ impl Editor {
         None
     }
 
+    fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+        let copilot = Copilot::global(cx)?;
+        if self.mode != EditorMode::Full {
+            return None;
+        }
+
+        self.copilot_state.completions.clear();
+        self.copilot_state.active_completion_index = 0;
+        self.copilot_state.position = Anchor::min();
+        self.display_map
+            .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
+        cx.notify();
+
+        if !copilot.read(cx).status().is_authorized() {
+            return None;
+        }
+
+        let selection = self.selections.newest_anchor();
+        let position = if selection.start == selection.end {
+            selection.start
+        } else {
+            return None;
+        };
+        let (buffer, buffer_position) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(position, cx)?;
+        self.copilot_state.position = position;
+        self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
+            cx.background().timer(COPILOT_TIMEOUT).await;
+            let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
+                (
+                    copilot.completion(&buffer, buffer_position, cx),
+                    copilot.completions_cycling(&buffer, buffer_position, cx),
+                )
+            });
+
+            if let Some(completion) = completion.await.log_err() {
+                let this = this.upgrade(&cx)?;
+                this.update(&mut cx, |this, cx| {
+                    if let Some(completion) = completion {
+                        this.display_map.update(cx, |map, cx| {
+                            map.replace_suggestion(
+                                Some(Suggestion {
+                                    position,
+                                    text: completion.text.as_str().into(),
+                                    highlight_style: HighlightStyle {
+                                        color: Some(Color::from_u32(0x777777ff)),
+                                        ..Default::default()
+                                    },
+                                }),
+                                cx,
+                            )
+                        });
+                        this.copilot_state.completions.push(completion);
+                        cx.notify();
+                    }
+                });
+            }
+
+            if let Some(completions) = completions_cycling.await.log_err() {
+                let this = this.upgrade(&cx)?;
+                this.update(&mut cx, |this, cx| {
+                    let was_empty = this.copilot_state.completions.is_empty();
+                    if !completions.is_empty() {
+                        if was_empty {
+                            let completion = completions.first().unwrap();
+                            this.display_map.update(cx, |map, cx| {
+                                map.replace_suggestion(
+                                    Some(Suggestion {
+                                        position,
+                                        text: completion.text.as_str().into(),
+                                        highlight_style: HighlightStyle {
+                                            color: Some(Color::from_u32(0x777777ff)),
+                                            ..Default::default()
+                                        },
+                                    }),
+                                    cx,
+                                )
+                            });
+                            cx.notify();
+                        }
+                        this.copilot_state.completions.extend(completions);
+                    }
+                });
+            }
+
+            Some(())
+        });
+
+        Some(())
+    }
+
+    fn cycle_copilot_suggestions(
+        &mut self,
+        _: &CycleCopilotSuggestions,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if self.copilot_state.completions.is_empty() {
+            return;
+        }
+
+        self.copilot_state.active_completion_index =
+            (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
+        if let Some(completion) = self.copilot_state.active_completion() {
+            self.display_map.update(cx, |map, cx| {
+                map.replace_suggestion(
+                    Some(Suggestion {
+                        position: self.copilot_state.position,
+                        text: completion.text.as_str().into(),
+                        highlight_style: HighlightStyle {
+                            color: Some(Color::from_u32(0x777777ff)),
+                            ..Default::default()
+                        },
+                    }),
+                    cx,
+                )
+            });
+        }
+
+        cx.notify();
+    }
+
     pub fn render_code_actions_indicator(
         &self,
         style: &EditorStyle,
@@ -2984,6 +3138,11 @@ impl Editor {
     }
 
     pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+        if let Some(completion) = self.copilot_state.active_completion() {
+            self.insert(&completion.text.to_string(), cx);
+            return;
+        }
+
         if self.move_to_next_snippet_tabstop(cx) {
             return;
         }

crates/gpui/src/platform/mac/window.rs 🔗

@@ -494,7 +494,6 @@ impl Window {
                         NSSize::new(rect.width() as f64, rect.height() as f64),
                     );
 
-                    dbg!(screen_frame.as_CGRect(), ns_rect.as_CGRect());
                     if ns_rect.intersects(screen_frame) {
                         native_window.setFrame_display_(ns_rect, YES);
                     } else {