Cargo.lock 🔗
@@ -1951,6 +1951,7 @@ dependencies = [
"clock",
"collections",
"context_menu",
+ "copilot",
"ctor",
"db",
"drag_and_drop",
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.
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(-)
@@ -1951,6 +1951,7 @@ dependencies = [
"clock",
"collections",
"context_menu",
+ "copilot",
"ctor",
"db",
"drag_and_drop",
@@ -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!(
@@ -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| {
@@ -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"
@@ -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;
}
@@ -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 {