Start integrating Copilot with editor
There's still a bit to do in terms of reusing the previous suggestion when the prefix matches, but we're getting there.
This commit is contained in:
parent
b16e2169ce
commit
2f95510a2e
6 changed files with 183 additions and 12 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1951,6 +1951,7 @@ dependencies = [
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"context_menu",
|
"context_menu",
|
||||||
|
"copilot",
|
||||||
"ctor",
|
"ctor",
|
||||||
"db",
|
"db",
|
||||||
"drag_and_drop",
|
"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));
|
let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx));
|
||||||
cx.set_global(copilot.clone());
|
cx.set_global(copilot.clone());
|
||||||
cx.add_global_action(|_: &SignIn, cx| {
|
cx.add_global_action(|_: &SignIn, cx| {
|
||||||
let copilot = Copilot::global(cx);
|
let copilot = Copilot::global(cx).unwrap();
|
||||||
copilot
|
copilot
|
||||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
});
|
});
|
||||||
cx.add_global_action(|_: &SignOut, cx| {
|
cx.add_global_action(|_: &SignOut, cx| {
|
||||||
let copilot = Copilot::global(cx);
|
let copilot = Copilot::global(cx).unwrap();
|
||||||
copilot
|
copilot
|
||||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
@ -75,13 +75,19 @@ pub enum Status {
|
||||||
Authorized,
|
Authorized,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Status {
|
||||||
|
pub fn is_authorized(&self) -> bool {
|
||||||
|
matches!(self, Status::Authorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Completion {
|
pub struct Completion {
|
||||||
pub position: Anchor,
|
pub position: Anchor,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Copilot {
|
pub struct Copilot {
|
||||||
server: CopilotServer,
|
server: CopilotServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,8 +96,12 @@ impl Entity for Copilot {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Copilot {
|
impl Copilot {
|
||||||
fn global(cx: &AppContext) -> ModelHandle<Self> {
|
pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
|
||||||
cx.global::<ModelHandle<Self>>().clone()
|
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 {
|
fn start(http: Arc<dyn HttpClient>, cx: &mut ModelContext<Self>) -> Self {
|
||||||
|
@ -240,7 +250,7 @@ impl Copilot {
|
||||||
where
|
where
|
||||||
T: ToPointUtf16,
|
T: ToPointUtf16,
|
||||||
{
|
{
|
||||||
let server = match self.authenticated_server() {
|
let server = match self.authorized_server() {
|
||||||
Ok(server) => server,
|
Ok(server) => server,
|
||||||
Err(error) => return Task::ready(Err(error)),
|
Err(error) => return Task::ready(Err(error)),
|
||||||
};
|
};
|
||||||
|
@ -268,7 +278,7 @@ impl Copilot {
|
||||||
where
|
where
|
||||||
T: ToPointUtf16,
|
T: ToPointUtf16,
|
||||||
{
|
{
|
||||||
let server = match self.authenticated_server() {
|
let server = match self.authorized_server() {
|
||||||
Ok(server) => server,
|
Ok(server) => server,
|
||||||
Err(error) => return Task::ready(Err(error)),
|
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 {
|
match &self.server {
|
||||||
CopilotServer::Downloading => Err(anyhow!("copilot is still downloading")),
|
CopilotServer::Downloading => Err(anyhow!("copilot is still downloading")),
|
||||||
CopilotServer::Error(error) => Err(anyhow!(
|
CopilotServer::Error(error) => Err(anyhow!(
|
||||||
|
|
|
@ -7,7 +7,7 @@ use gpui::{
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
let copilot = Copilot::global(cx);
|
let copilot = Copilot::global(cx).unwrap();
|
||||||
|
|
||||||
let mut code_verification_window_id = None;
|
let mut code_verification_window_id = None;
|
||||||
cx.observe(&copilot, move |copilot, cx| {
|
cx.observe(&copilot, move |copilot, cx| {
|
||||||
|
|
|
@ -22,10 +22,10 @@ test-support = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
drag_and_drop = { path = "../drag_and_drop" }
|
|
||||||
text = { path = "../text" }
|
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
|
copilot = { path = "../copilot" }
|
||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
|
drag_and_drop = { path = "../drag_and_drop" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
@ -38,10 +38,12 @@ rpc = { path = "../rpc" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
snippet = { path = "../snippet" }
|
snippet = { path = "../snippet" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
text = { path = "../text" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
sqlez = { path = "../sqlez" }
|
sqlez = { path = "../sqlez" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
|
|
||||||
aho-corasick = "0.7"
|
aho-corasick = "0.7"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
|
|
@ -24,6 +24,7 @@ use anyhow::Result;
|
||||||
use blink_manager::BlinkManager;
|
use blink_manager::BlinkManager;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||||
|
use copilot::Copilot;
|
||||||
pub use display_map::DisplayPoint;
|
pub use display_map::DisplayPoint;
|
||||||
use display_map::*;
|
use display_map::*;
|
||||||
pub use element::*;
|
pub use element::*;
|
||||||
|
@ -96,6 +97,7 @@ const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
||||||
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
||||||
|
|
||||||
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||||
|
pub const COPILOT_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq, Default)]
|
#[derive(Clone, Deserialize, PartialEq, Default)]
|
||||||
pub struct SelectNext {
|
pub struct SelectNext {
|
||||||
|
@ -260,6 +262,7 @@ actions!(
|
||||||
ToggleSoftWrap,
|
ToggleSoftWrap,
|
||||||
RevealInFinder,
|
RevealInFinder,
|
||||||
CopyHighlightJson
|
CopyHighlightJson
|
||||||
|
CycleCopilotSuggestions
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -388,6 +391,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_async_action(Editor::rename);
|
cx.add_async_action(Editor::rename);
|
||||||
cx.add_async_action(Editor::confirm_rename);
|
cx.add_async_action(Editor::confirm_rename);
|
||||||
cx.add_async_action(Editor::find_all_references);
|
cx.add_async_action(Editor::find_all_references);
|
||||||
|
cx.add_action(Editor::cycle_copilot_suggestions);
|
||||||
|
|
||||||
hover_popover::init(cx);
|
hover_popover::init(cx);
|
||||||
link_go_to_definition::init(cx);
|
link_go_to_definition::init(cx);
|
||||||
|
@ -506,6 +510,7 @@ pub struct Editor {
|
||||||
hover_state: HoverState,
|
hover_state: HoverState,
|
||||||
gutter_hovered: bool,
|
gutter_hovered: bool,
|
||||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||||
|
copilot_state: CopilotState,
|
||||||
_subscriptions: Vec<Subscription>,
|
_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)]
|
#[derive(Debug)]
|
||||||
struct ActiveDiagnosticGroup {
|
struct ActiveDiagnosticGroup {
|
||||||
primary_range: Range<Anchor>,
|
primary_range: Range<Anchor>,
|
||||||
|
@ -1176,6 +1205,7 @@ impl Editor {
|
||||||
remote_id: None,
|
remote_id: None,
|
||||||
hover_state: Default::default(),
|
hover_state: Default::default(),
|
||||||
link_go_to_definition_state: Default::default(),
|
link_go_to_definition_state: Default::default(),
|
||||||
|
copilot_state: Default::default(),
|
||||||
gutter_hovered: false,
|
gutter_hovered: false,
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
cx.observe(&buffer, Self::on_buffer_changed),
|
cx.observe(&buffer, Self::on_buffer_changed),
|
||||||
|
@ -1385,6 +1415,7 @@ impl Editor {
|
||||||
self.refresh_code_actions(cx);
|
self.refresh_code_actions(cx);
|
||||||
self.refresh_document_highlights(cx);
|
self.refresh_document_highlights(cx);
|
||||||
refresh_matching_bracket_highlights(self, cx);
|
refresh_matching_bracket_highlights(self, cx);
|
||||||
|
self.refresh_copilot_suggestions(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||||
|
@ -2677,6 +2708,129 @@ impl Editor {
|
||||||
None
|
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(
|
pub fn render_code_actions_indicator(
|
||||||
&self,
|
&self,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
|
@ -2984,6 +3138,11 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
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) {
|
if self.move_to_next_snippet_tabstop(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -494,7 +494,6 @@ impl Window {
|
||||||
NSSize::new(rect.width() as f64, rect.height() as f64),
|
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) {
|
if ns_rect.intersects(screen_frame) {
|
||||||
native_window.setFrame_display_(ns_rect, YES);
|
native_window.setFrame_display_(ns_rect, YES);
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue