Detailed changes
@@ -2442,13 +2442,20 @@ name = "copilot_ui"
version = "0.1.0"
dependencies = [
"anyhow",
+ "client",
"copilot",
"editor",
"fs",
+ "futures 0.3.28",
"gpui",
+ "indoc",
"language",
+ "lsp",
"menu",
+ "project",
+ "serde_json",
"settings",
+ "theme",
"ui",
"util",
"workspace",
@@ -3215,7 +3222,6 @@ dependencies = [
"clock",
"collections",
"convert_case 0.6.0",
- "copilot",
"ctor",
"db",
"emojis",
@@ -140,17 +140,17 @@
}
},
{
- "context": "Editor && mode == full && copilot_suggestion",
+ "context": "Editor && mode == full && inline_completion",
"bindings": {
- "alt-]": "copilot::NextSuggestion",
- "alt-[": "copilot::PreviousSuggestion",
- "alt-right": "editor::AcceptPartialCopilotSuggestion"
+ "alt-]": "editor::NextInlineCompletion",
+ "alt-[": "editor::PreviousInlineCompletion",
+ "alt-right": "editor::AcceptPartialInlineCompletion"
}
},
{
- "context": "Editor && !copilot_suggestion",
+ "context": "Editor && !inline_completion",
"bindings": {
- "alt-\\": "copilot::Suggest"
+ "alt-\\": "editor::ShowInlineCompletion"
}
},
{
@@ -182,17 +182,17 @@
}
},
{
- "context": "Editor && mode == full && copilot_suggestion",
+ "context": "Editor && mode == full && inline_completion",
"bindings": {
- "alt-]": "copilot::NextSuggestion",
- "alt-[": "copilot::PreviousSuggestion",
- "alt-right": "editor::AcceptPartialCopilotSuggestion"
+ "alt-]": "editor::NextInlineCompletion",
+ "alt-[": "editor::PreviousInlineCompletion",
+ "alt-right": "editor::AcceptPartialInlineCompletion"
}
},
{
- "context": "Editor && !copilot_suggestion",
+ "context": "Editor && !inline_completion",
"bindings": {
- "alt-\\": "copilot::Suggest"
+ "alt-\\": "editor::ShowInlineCompletion"
}
},
{
@@ -510,7 +510,7 @@
"ctrl-[": "vim::NormalBefore",
"ctrl-x ctrl-o": "editor::ShowCompletions",
"ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
- "ctrl-x ctrl-c": "copilot::Suggest", // zed specific
+ "ctrl-x ctrl-c": "editor::ShowInlineCompletion", // zed specific
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
"ctrl-x ctrl-z": "editor::Cancel",
"ctrl-w": "editor::DeleteToPreviousWordStart",
@@ -12,7 +12,7 @@ use chrono::{DateTime, Local};
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
-use gpui::{actions, AppContext, Global, SharedString};
+use gpui::{actions, AppContext, BorrowAppContext, Global, SharedString};
pub(crate) use saved_conversation::*;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@@ -383,7 +383,7 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
#[cfg(test)]
mod tests {
- use gpui::AppContext;
+ use gpui::{AppContext, BorrowAppContext};
use settings::SettingsStore;
use super::*;
@@ -15,7 +15,7 @@ use crate::{
use anyhow::Result;
use client::Client;
use futures::{future::BoxFuture, stream::BoxStream};
-use gpui::{AnyView, AppContext, Task, WindowContext};
+use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext};
use settings::{Settings, SettingsStore};
use std::sync::Arc;
@@ -1,6 +1,6 @@
use assets::SoundRegistry;
use derive_more::{Deref, DerefMut};
-use gpui::{AppContext, AssetSource, Global};
+use gpui::{AppContext, AssetSource, BorrowAppContext, Global};
use rodio::{OutputStream, OutputStreamHandle};
use util::ResultExt;
@@ -17,7 +17,8 @@ use futures::{
TryFutureExt as _, TryStreamExt,
};
use gpui::{
- actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
+ actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, BorrowAppContext, Global, Model,
+ Task, WeakModel,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
@@ -12,7 +12,7 @@ use editor::{
Editor,
};
use futures::StreamExt;
-use gpui::{TestAppContext, VisualContext, VisualTestContext};
+use gpui::{BorrowAppContext, TestAppContext, VisualContext, VisualTestContext};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, InlayHintSettings},
@@ -7,8 +7,8 @@ use collab_ui::{
};
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{
- point, BackgroundExecutor, Context, Entity, SharedString, TestAppContext, View, VisualContext,
- VisualTestContext,
+ point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
+ View, VisualContext, VisualTestContext,
};
use language::Capability;
use live_kit_client::MacOSDisplay;
@@ -8,8 +8,8 @@ use collections::{HashMap, HashSet};
use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
use futures::StreamExt as _;
use gpui::{
- px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
- TestAppContext,
+ px, size, AppContext, BackgroundExecutor, BorrowAppContext, Model, Modifiers, MouseButton,
+ MouseDownEvent, TestAppContext,
};
use language::{
language_settings::{AllLanguageSettings, Formatter},
@@ -6,7 +6,7 @@ use std::any::TypeId;
use collections::HashSet;
use derive_more::{Deref, DerefMut};
-use gpui::{Action, AppContext, Global};
+use gpui::{Action, AppContext, BorrowAppContext, Global};
/// Initializes the command palette hooks.
pub fn init(cx: &mut AppContext) {
@@ -14,6 +14,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
+client.workspace = true
copilot.workspace = true
editor.workspace = true
fs.workspace = true
@@ -27,4 +28,11 @@ workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
+copilot = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
+futures.workspace = true
+indoc.workspace = true
+lsp = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+serde_json.workspace = true
+theme = { workspace = true, features = ["test-support"] }
@@ -0,0 +1,1021 @@
+use anyhow::Result;
+use client::telemetry::Telemetry;
+use copilot::Copilot;
+use editor::{Direction, InlineCompletionProvider};
+use gpui::{AppContext, EntityId, Model, ModelContext, Task};
+use language::{language_settings::all_language_settings, Buffer, OffsetRangeExt, ToOffset};
+use std::{path::Path, sync::Arc, time::Duration};
+
+pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
+
+pub struct CopilotCompletionProvider {
+ cycled: bool,
+ buffer_id: Option<EntityId>,
+ completions: Vec<copilot::Completion>,
+ active_completion_index: usize,
+ file_extension: Option<String>,
+ pending_refresh: Task<Result<()>>,
+ pending_cycling_refresh: Task<Result<()>>,
+ copilot: Model<Copilot>,
+ telemetry: Option<Arc<Telemetry>>,
+}
+
+impl CopilotCompletionProvider {
+ pub fn new(copilot: Model<Copilot>) -> Self {
+ Self {
+ cycled: false,
+ buffer_id: None,
+ completions: Vec::new(),
+ active_completion_index: 0,
+ file_extension: None,
+ pending_refresh: Task::ready(Ok(())),
+ pending_cycling_refresh: Task::ready(Ok(())),
+ copilot,
+ telemetry: None,
+ }
+ }
+
+ pub fn with_telemetry(mut self, telemetry: Arc<Telemetry>) -> Self {
+ self.telemetry = Some(telemetry);
+ self
+ }
+
+ fn active_completion(&self) -> Option<&copilot::Completion> {
+ self.completions.get(self.active_completion_index)
+ }
+
+ fn push_completion(&mut self, new_completion: copilot::Completion) {
+ for completion in &self.completions {
+ if completion.text == new_completion.text && completion.range == new_completion.range {
+ return;
+ }
+ }
+ self.completions.push(new_completion);
+ }
+}
+
+impl InlineCompletionProvider for CopilotCompletionProvider {
+ fn is_enabled(
+ &self,
+ buffer: &Model<Buffer>,
+ cursor_position: language::Anchor,
+ cx: &AppContext,
+ ) -> bool {
+ if !self.copilot.read(cx).status().is_authorized() {
+ return false;
+ }
+
+ let buffer = buffer.read(cx);
+ let file = buffer.file();
+ let language = buffer.language_at(cursor_position);
+ let settings = all_language_settings(file, cx);
+ settings.copilot_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
+ }
+
+ fn refresh(
+ &mut self,
+ buffer: Model<Buffer>,
+ cursor_position: language::Anchor,
+ debounce: bool,
+ cx: &mut ModelContext<Self>,
+ ) {
+ let copilot = self.copilot.clone();
+ self.pending_refresh = cx.spawn(|this, mut cx| async move {
+ if debounce {
+ cx.background_executor()
+ .timer(COPILOT_DEBOUNCE_TIMEOUT)
+ .await;
+ }
+
+ let completions = copilot
+ .update(&mut cx, |copilot, cx| {
+ copilot.completions(&buffer, cursor_position, cx)
+ })?
+ .await?;
+
+ this.update(&mut cx, |this, cx| {
+ if !completions.is_empty() {
+ this.cycled = false;
+ this.pending_cycling_refresh = Task::ready(Ok(()));
+ this.completions.clear();
+ this.active_completion_index = 0;
+ this.buffer_id = Some(buffer.entity_id());
+ this.file_extension = buffer.read(cx).file().and_then(|file| {
+ Some(
+ Path::new(file.file_name(cx))
+ .extension()?
+ .to_str()?
+ .to_string(),
+ )
+ });
+
+ for completion in completions {
+ this.push_completion(completion);
+ }
+ cx.notify();
+ }
+ })?;
+
+ Ok(())
+ });
+ }
+
+ fn cycle(
+ &mut self,
+ buffer: Model<Buffer>,
+ cursor_position: language::Anchor,
+ direction: Direction,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if self.cycled {
+ match direction {
+ Direction::Prev => {
+ self.active_completion_index = if self.active_completion_index == 0 {
+ self.completions.len().saturating_sub(1)
+ } else {
+ self.active_completion_index - 1
+ };
+ }
+ Direction::Next => {
+ if self.completions.len() == 0 {
+ self.active_completion_index = 0
+ } else {
+ self.active_completion_index =
+ (self.active_completion_index + 1) % self.completions.len();
+ }
+ }
+ }
+
+ cx.notify();
+ } else {
+ let copilot = self.copilot.clone();
+ self.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {
+ let completions = copilot
+ .update(&mut cx, |copilot, cx| {
+ copilot.completions_cycling(&buffer, cursor_position, cx)
+ })?
+ .await?;
+
+ this.update(&mut cx, |this, cx| {
+ this.cycled = true;
+ this.file_extension = buffer.read(cx).file().and_then(|file| {
+ Some(
+ Path::new(file.file_name(cx))
+ .extension()?
+ .to_str()?
+ .to_string(),
+ )
+ });
+ for completion in completions {
+ this.push_completion(completion);
+ }
+ this.cycle(buffer, cursor_position, direction, cx);
+ })?;
+
+ Ok(())
+ });
+ }
+ }
+
+ fn accept(&mut self, cx: &mut ModelContext<Self>) {
+ if let Some(completion) = self.active_completion() {
+ self.copilot
+ .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
+ .detach_and_log_err(cx);
+ if let Some(telemetry) = self.telemetry.as_ref() {
+ telemetry.report_copilot_event(
+ Some(completion.uuid.clone()),
+ true,
+ self.file_extension.clone(),
+ );
+ }
+ }
+ }
+
+ fn discard(&mut self, cx: &mut ModelContext<Self>) {
+ self.copilot
+ .update(cx, |copilot, cx| {
+ copilot.discard_completions(&self.completions, cx)
+ })
+ .detach_and_log_err(cx);
+ if let Some(telemetry) = self.telemetry.as_ref() {
+ telemetry.report_copilot_event(None, false, self.file_extension.clone());
+ }
+ }
+
+ fn active_completion_text(
+ &self,
+ buffer: &Model<Buffer>,
+ cursor_position: language::Anchor,
+ cx: &AppContext,
+ ) -> Option<&str> {
+ let buffer_id = buffer.entity_id();
+ let buffer = buffer.read(cx);
+ let completion = self.active_completion()?;
+ if Some(buffer_id) != self.buffer_id
+ || !completion.range.start.is_valid(buffer)
+ || !completion.range.end.is_valid(buffer)
+ {
+ return None;
+ }
+
+ let mut completion_range = completion.range.to_offset(buffer);
+ let prefix_len = common_prefix(
+ buffer.chars_for_range(completion_range.clone()),
+ completion.text.chars(),
+ );
+ completion_range.start += prefix_len;
+ let suffix_len = common_prefix(
+ buffer.reversed_chars_for_range(completion_range.clone()),
+ completion.text[prefix_len..].chars().rev(),
+ );
+ completion_range.end = completion_range.end.saturating_sub(suffix_len);
+
+ if completion_range.is_empty()
+ && completion_range.start == cursor_position.to_offset(buffer)
+ {
+ let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
+ if completion_text.trim().is_empty() {
+ None
+ } else {
+ Some(completion_text)
+ }
+ } else {
+ None
+ }
+ }
+}
+
+fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
+ a.zip(b)
+ .take_while(|(a, b)| a == b)
+ .map(|(a, _)| a.len_utf8())
+ .sum()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use editor::{
+ test::editor_lsp_test_context::EditorLspTestContext, Editor, ExcerptRange, MultiBuffer,
+ };
+ use fs::FakeFs;
+ use futures::StreamExt;
+ use gpui::{BackgroundExecutor, BorrowAppContext, Context, TestAppContext};
+ use indoc::indoc;
+ use language::{
+ language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
+ BufferId, Point,
+ };
+ use project::Project;
+ use serde_json::json;
+ use settings::SettingsStore;
+ use std::future::Future;
+ use util::test::{marked_text_ranges_by, TextRangeMarker};
+
+ #[gpui::test(iterations = 10)]
+ async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ // flaky
+ init_test(cx, |_| {});
+
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+ 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;
+ let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
+ cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
+
+ // When inserting, ensure autocompletion is favored over Copilot suggestions.
+ cx.set_state(indoc! {"
+ oneΛ
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ let _ = handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three
+ "},
+ vec!["completion_a", "completion_b"],
+ );
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.copilot1".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.context_menu_visible());
+ assert!(!editor.has_active_inline_completion(cx));
+
+ // Confirming a completion inserts it and hides the context menu, without showing
+ // the copilot suggestion afterwards.
+ editor
+ .confirm_completion(&Default::default(), cx)
+ .unwrap()
+ .detach();
+ assert!(!editor.context_menu_visible());
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
+ assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
+ });
+
+ // Ensure Copilot suggestions are shown right away if no autocompletion is available.
+ cx.set_state(indoc! {"
+ oneΛ
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ let _ = handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three
+ "},
+ vec![],
+ );
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.copilot1".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(!editor.context_menu_visible());
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
+ });
+
+ // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
+ cx.set_state(indoc! {"
+ oneΛ
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ let _ = handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three
+ "},
+ vec!["completion_a", "completion_b"],
+ );
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.copilot1".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.context_menu_visible());
+ assert!(!editor.has_active_inline_completion(cx));
+
+ // When hiding the context menu, the Copilot suggestion becomes visible.
+ editor.cancel(&Default::default(), cx);
+ assert!(!editor.context_menu_visible());
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
+ });
+
+ // Ensure existing completion is interpolated when inserting again.
+ cx.simulate_keystroke("c");
+ executor.run_until_parked();
+ cx.update_editor(|editor, cx| {
+ assert!(!editor.context_menu_visible());
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+ });
+
+ // After debouncing, new Copilot completions should be requested.
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.copilot2".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(!editor.context_menu_visible());
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+
+ // Canceling should remove the active Copilot suggestion.
+ editor.cancel(&Default::default(), cx);
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+
+ // After canceling, tabbing shouldn't insert the previously shown suggestion.
+ editor.tab(&Default::default(), cx);
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
+
+ // When undoing the previously active suggestion is shown again.
+ editor.undo(&Default::default(), cx);
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+ });
+
+ // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
+ cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
+ cx.update_editor(|editor, cx| {
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
+
+ // Tabbing when there is an active suggestion inserts it.
+ editor.tab(&Default::default(), cx);
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
+
+ // When undoing the previously active suggestion is shown again.
+ editor.undo(&Default::default(), cx);
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
+
+ // Hide suggestion.
+ editor.cancel(&Default::default(), cx);
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
+ });
+
+ // If an edit occurs outside of this editor but no suggestion is being shown,
+ // we won't make it visible.
+ cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
+ cx.update_editor(|editor, cx| {
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
+ });
+
+ // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
+ cx.update_editor(|editor, cx| {
+ editor.set_text("fn foo() {\n \n}", cx);
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
+ });
+ });
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: " let x = 4;".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_inline_completion(&Default::default(), cx));
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
+ assert_eq!(editor.text(cx), "fn foo() {\n \n}");
+
+ // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
+ editor.tab(&Default::default(), cx);
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.text(cx), "fn foo() {\n \n}");
+ assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
+
+ // Tabbing again accepts the suggestion.
+ editor.tab(&Default::default(), cx);
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
+ assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
+ });
+ }
+
+ #[gpui::test(iterations = 10)]
+ async fn test_accept_partial_copilot_suggestion(
+ executor: BackgroundExecutor,
+ cx: &mut TestAppContext,
+ ) {
+ // flaky
+ init_test(cx, |_| {});
+
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+ 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;
+ let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
+ cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
+
+ // Setup the editor with a completion request.
+ cx.set_state(indoc! {"
+ oneΛ
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ let _ = handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three
+ "},
+ vec![],
+ );
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.copilot1".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.has_active_inline_completion(cx));
+
+ // Accepting the first word of the suggestion should only accept the first word and still show the rest.
+ editor.accept_partial_inline_completion(&Default::default(), cx);
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
+ assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+
+ // Accepting next word should accept the non-word and copilot suggestion should be gone
+ editor.accept_partial_inline_completion(&Default::default(), cx);
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
+ assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+ });
+
+ // Reset the editor and check non-word and whitespace completion
+ cx.set_state(indoc! {"
+ oneΛ
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ let _ = handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three
+ "},
+ vec![],
+ );
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.123. copilot\n 456".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.has_active_inline_completion(cx));
+
+ // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
+ editor.accept_partial_inline_completion(&Default::default(), cx);
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
+ assert_eq!(
+ editor.display_text(cx),
+ "one.123. copilot\n 456\ntwo\nthree\n"
+ );
+
+ // Accepting next word should accept the next word and copilot suggestion should still exist
+ editor.accept_partial_inline_completion(&Default::default(), cx);
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
+ assert_eq!(
+ editor.display_text(cx),
+ "one.123. copilot\n 456\ntwo\nthree\n"
+ );
+
+ // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
+ editor.accept_partial_inline_completion(&Default::default(), cx);
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
+ assert_eq!(
+ editor.display_text(cx),
+ "one.123. copilot\n 456\ntwo\nthree\n"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_copilot_completion_invalidation(
+ executor: BackgroundExecutor,
+ cx: &mut TestAppContext,
+ ) {
+ init_test(cx, |_| {});
+
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+ 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;
+ let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
+ cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
+
+ 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_inline_completion(&Default::default(), cx));
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.has_active_inline_completion(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_inline_completion(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_inline_completion(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_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one\nthree\n");
+ assert_eq!(editor.text(cx), "one\nthree\n");
+
+ // Undoing the deletion restores the suggestion.
+ editor.undo(&Default::default(), cx);
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
+ assert_eq!(editor.text(cx), "one\n\nthree\n");
+ });
+ }
+
+ #[gpui::test]
+ async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+
+ let buffer_1 = cx.new_model(|cx| {
+ Buffer::new(
+ 0,
+ BufferId::new(cx.entity_id().as_u64()).unwrap(),
+ "a = 1\nb = 2\n",
+ )
+ });
+ let buffer_2 = cx.new_model(|cx| {
+ Buffer::new(
+ 0,
+ BufferId::new(cx.entity_id().as_u64()).unwrap(),
+ "c = 3\nd = 4\n",
+ )
+ });
+ let multibuffer = cx.new_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0, language::Capability::ReadWrite);
+ multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(2, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(2, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer
+ });
+ let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx));
+ let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
+ editor
+ .update(cx, |editor, cx| {
+ editor.set_inline_completion_provider(copilot_provider, cx)
+ })
+ .unwrap();
+
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "b = 2 + a".into(),
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ _ = editor.update(cx, |editor, cx| {
+ // Ensure copilot suggestions are shown for the first excerpt.
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
+ });
+ editor.next_inline_completion(&Default::default(), cx);
+ });
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ _ = editor.update(cx, |editor, cx| {
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(
+ editor.display_text(cx),
+ "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
+ );
+ assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
+ });
+
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "d = 4 + c".into(),
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ _ = editor.update(cx, |editor, cx| {
+ // Move to another excerpt, ensuring the suggestion gets cleared.
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
+ });
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(
+ editor.display_text(cx),
+ "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
+ );
+ assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
+
+ // Type a character, ensuring we don't even try to interpolate the previous suggestion.
+ editor.handle_input(" ", cx);
+ assert!(!editor.has_active_inline_completion(cx));
+ assert_eq!(
+ editor.display_text(cx),
+ "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
+ );
+ assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
+ });
+
+ // Ensure the new suggestion is displayed when the debounce timeout expires.
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ _ = editor.update(cx, |editor, cx| {
+ assert!(editor.has_active_inline_completion(cx));
+ assert_eq!(
+ editor.display_text(cx),
+ "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
+ );
+ assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
+ });
+ }
+
+ #[gpui::test]
+ async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx, |settings| {
+ settings
+ .copilot
+ .get_or_insert(Default::default())
+ .disabled_globs = Some(vec![".env*".to_string()]);
+ });
+
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/test",
+ json!({
+ ".env": "SECRET=something\n",
+ "README.md": "hello\n"
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/test".as_ref()], cx).await;
+
+ let private_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/test/.env", cx)
+ })
+ .await
+ .unwrap();
+ let public_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/test/README.md", cx)
+ })
+ .await
+ .unwrap();
+
+ let multibuffer = cx.new_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0, language::Capability::ReadWrite);
+ multibuffer.push_excerpts(
+ private_buffer.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ public_buffer.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer
+ });
+ let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx));
+ let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
+ editor
+ .update(cx, |editor, cx| {
+ editor.set_inline_completion_provider(copilot_provider, cx)
+ })
+ .unwrap();
+
+ let mut copilot_requests = copilot_lsp
+ .handle_request::<copilot::request::GetCompletions, _, _>(
+ move |_params, _cx| async move {
+ Ok(copilot::request::GetCompletionsResult {
+ completions: vec![copilot::request::Completion {
+ text: "next line".into(),
+ range: lsp::Range::new(
+ lsp::Position::new(1, 0),
+ lsp::Position::new(1, 0),
+ ),
+ ..Default::default()
+ }],
+ })
+ },
+ );
+
+ _ = editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
+ });
+ editor.next_inline_completion(&Default::default(), cx);
+ });
+
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ assert!(copilot_requests.try_next().is_err());
+
+ _ = editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
+ });
+ editor.next_inline_completion(&Default::default(), cx);
+ });
+
+ executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ assert!(copilot_requests.try_next().is_ok());
+ }
+
+ fn handle_copilot_completion_request(
+ lsp: &lsp::FakeLanguageServer,
+ completions: Vec<copilot::request::Completion>,
+ completions_cycling: Vec<copilot::request::Completion>,
+ ) {
+ lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
+ let completions = completions.clone();
+ async move {
+ Ok(copilot::request::GetCompletionsResult {
+ completions: completions.clone(),
+ })
+ }
+ });
+ lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
+ let completions_cycling = completions_cycling.clone();
+ async move {
+ Ok(copilot::request::GetCompletionsResult {
+ completions: completions_cycling.clone(),
+ })
+ }
+ });
+ }
+
+ fn handle_completion_request(
+ cx: &mut EditorLspTestContext,
+ marked_string: &str,
+ completions: Vec<&'static str>,
+ ) -> impl Future<Output = ()> {
+ let complete_from_marker: TextRangeMarker = '|'.into();
+ let replace_range_marker: TextRangeMarker = ('<', '>').into();
+ let (_, mut marked_ranges) = marked_text_ranges_by(
+ marked_string,
+ vec![complete_from_marker.clone(), replace_range_marker.clone()],
+ );
+
+ let complete_from_position =
+ cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
+ let replace_range =
+ cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
+
+ let mut request =
+ cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
+ let completions = completions.clone();
+ async move {
+ assert_eq!(params.text_document_position.text_document.uri, url.clone());
+ assert_eq!(
+ params.text_document_position.position,
+ complete_from_position
+ );
+ Ok(Some(lsp::CompletionResponse::Array(
+ completions
+ .iter()
+ .map(|completion_text| lsp::CompletionItem {
+ label: completion_text.to_string(),
+ text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+ range: replace_range,
+ new_text: completion_text.to_string(),
+ })),
+ ..Default::default()
+ })
+ .collect(),
+ )))
+ }
+ });
+
+ async move {
+ request.next().await;
+ }
+ }
+
+ fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
+ _ = cx.update(|cx| {
+ let store = SettingsStore::test(cx);
+ cx.set_global(store);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ client::init_settings(cx);
+ language::init(cx);
+ editor::init_settings(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, f);
+ });
+ });
+ }
+}
@@ -1,5 +1,7 @@
pub mod copilot_button;
+mod copilot_completion_provider;
mod sign_in;
pub use copilot_button::*;
+pub use copilot_completion_provider::*;
pub use sign_in::*;
@@ -14,7 +14,6 @@ doctest = false
[features]
test-support = [
- "copilot/test-support",
"text/test-support",
"language/test-support",
"gpui/test-support",
@@ -34,7 +33,6 @@ client.workspace = true
clock.workspace = true
collections.workspace = true
convert_case = "0.6.0"
-copilot.workspace = true
db.workspace = true
emojis.workspace = true
futures.workspace = true
@@ -73,7 +71,6 @@ util.workspace = true
workspace.workspace = true
[dev-dependencies]
-copilot = { workspace = true, features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
@@ -127,6 +127,7 @@ gpui::actions!(
editor,
[
AcceptPartialCopilotSuggestion,
+ AcceptPartialInlineCompletion,
AddSelectionAbove,
AddSelectionBelow,
Backspace,
@@ -168,13 +169,12 @@ gpui::actions!(
GoToDefinitionSplit,
GoToDiagnostic,
GoToHunk,
+ GoToImplementation,
+ GoToImplementationSplit,
GoToPrevDiagnostic,
GoToPrevHunk,
GoToTypeDefinition,
GoToTypeDefinitionSplit,
- GoToImplementation,
- GoToImplementationSplit,
- OpenUrl,
HalfPageDown,
HalfPageUp,
Hover,
@@ -202,21 +202,24 @@ gpui::actions!(
Newline,
NewlineAbove,
NewlineBelow,
+ NextInlineCompletion,
NextScreen,
OpenExcerpts,
OpenExcerptsSplit,
OpenPermalinkToLine,
+ OpenUrl,
Outdent,
PageDown,
PageUp,
Paste,
- RevertSelectedHunks,
+ PreviousInlineCompletion,
Redo,
RedoSelection,
Rename,
RestartLanguageServer,
RevealInFinder,
ReverseLines,
+ RevertSelectedHunks,
ScrollCursorBottom,
ScrollCursorCenter,
ScrollCursorTop,
@@ -239,6 +242,7 @@ gpui::actions!(
SelectUp,
ShowCharacterPalette,
ShowCompletions,
+ ShowInlineCompletion,
ShuffleLines,
SortLinesCaseInsensitive,
SortLinesCaseSensitive,
@@ -246,8 +250,8 @@ gpui::actions!(
Tab,
TabPrev,
ToggleInlayHints,
- ToggleSoftWrap,
ToggleLineNumbers,
+ ToggleSoftWrap,
Transpose,
Undo,
UndoSelection,
@@ -1015,7 +1015,7 @@ pub mod tests {
movement,
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
};
- use gpui::{div, font, observe, px, AppContext, Context, Element, Hsla};
+ use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, LanguageMatcher, SelectionGoal,
@@ -24,6 +24,7 @@ mod git;
mod highlight_matching_bracket;
mod hover_links;
mod hover_popover;
+mod inline_completion_provider;
pub mod items;
mod mouse_context_menu;
pub mod movement;
@@ -45,7 +46,6 @@ use client::{Collaborator, ParticipantIndex};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
-use copilot::Copilot;
use debounced_delay::DebouncedDelay;
pub use display_map::DisplayPoint;
use display_map::*;
@@ -69,6 +69,7 @@ use gpui::{
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
+pub use inline_completion_provider::*;
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{char_kind, CharKind};
@@ -135,7 +136,6 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
-const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
@@ -419,7 +419,9 @@ pub struct Editor {
hover_state: HoverState,
gutter_hovered: bool,
hovered_link_state: Option<HoveredLinkState>,
- copilot_state: CopilotState,
+ inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
+ active_inline_completion: Option<Inlay>,
+ show_inline_completions: bool,
inlay_hint_cache: InlayHintCache,
next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
@@ -428,7 +430,6 @@ pub struct Editor {
pub vim_replace_map: HashMap<Range<usize>, String>,
style: Option<EditorStyle>,
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
- show_copilot_suggestions: bool,
use_autoclose: bool,
auto_replace_emoji_shortcode: bool,
custom_context_menu: Option<
@@ -625,6 +626,11 @@ pub struct RenameState {
struct InvalidationStack<T>(Vec<T>);
+struct RegisteredInlineCompletionProvider {
+ provider: Arc<dyn InlineCompletionProviderHandle>,
+ _subscription: Subscription,
+}
+
enum ContextMenu {
Completions(CompletionsMenu),
CodeActions(CodeActionsMenu),
@@ -1230,116 +1236,6 @@ impl CodeActionsMenu {
}
}
-#[derive(Debug)]
-pub(crate) struct CopilotState {
- excerpt_id: Option<ExcerptId>,
- pending_refresh: Task<Option<()>>,
- pending_cycling_refresh: Task<Option<()>>,
- cycled: bool,
- completions: Vec<copilot::Completion>,
- active_completion_index: usize,
- suggestion: Option<Inlay>,
-}
-
-impl Default for CopilotState {
- fn default() -> Self {
- Self {
- excerpt_id: None,
- pending_cycling_refresh: Task::ready(Some(())),
- pending_refresh: Task::ready(Some(())),
- completions: Default::default(),
- active_completion_index: 0,
- cycled: false,
- suggestion: None,
- }
- }
-}
-
-impl CopilotState {
- fn active_completion(&self) -> Option<&copilot::Completion> {
- self.completions.get(self.active_completion_index)
- }
-
- fn text_for_active_completion(
- &self,
- cursor: Anchor,
- buffer: &MultiBufferSnapshot,
- ) -> Option<&str> {
- use language::ToOffset as _;
-
- let completion = self.active_completion()?;
- let excerpt_id = self.excerpt_id?;
- let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
- if excerpt_id != cursor.excerpt_id
- || !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(
- completion_buffer.chars_for_range(completion_range.clone()),
- completion.text.chars(),
- );
- completion_range.start += prefix_len;
- let suffix_len = Self::common_prefix(
- completion_buffer.reversed_chars_for_range(completion_range.clone()),
- completion.text[prefix_len..].chars().rev(),
- );
- completion_range.end = completion_range.end.saturating_sub(suffix_len);
-
- if completion_range.is_empty()
- && completion_range.start == cursor.text_anchor.to_offset(&completion_buffer)
- {
- let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
- if completion_text.trim().is_empty() {
- None
- } else {
- Some(completion_text)
- }
- } else {
- None
- }
- }
-
- fn cycle_completions(&mut self, direction: Direction) {
- match direction {
- Direction::Prev => {
- self.active_completion_index = if self.active_completion_index == 0 {
- self.completions.len().saturating_sub(1)
- } else {
- self.active_completion_index - 1
- };
- }
- Direction::Next => {
- if self.completions.len() == 0 {
- self.active_completion_index = 0
- } else {
- self.active_completion_index =
- (self.active_completion_index + 1) % self.completions.len();
- }
- }
- }
- }
-
- fn push_completion(&mut self, new_completion: copilot::Completion) {
- for completion in &self.completions {
- if completion.text == new_completion.text && completion.range == new_completion.range {
- return;
- }
- }
- self.completions.push(new_completion);
- }
-
- fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
- a.zip(b)
- .take_while(|(a, b)| a == b)
- .map(|(a, _)| a.len_utf8())
- .sum()
- }
-}
-
#[derive(Debug)]
struct ActiveDiagnosticGroup {
primary_range: Range<Anchor>,
@@ -1562,7 +1458,8 @@ impl Editor {
remote_id: None,
hover_state: Default::default(),
hovered_link_state: Default::default(),
- copilot_state: Default::default(),
+ inline_completion_provider: None,
+ active_inline_completion: None,
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
@@ -1572,7 +1469,7 @@ impl Editor {
hovered_cursors: Default::default(),
editor_actions: Default::default(),
vim_replace_map: Default::default(),
- show_copilot_suggestions: mode == EditorMode::Full,
+ show_inline_completions: mode == EditorMode::Full,
custom_context_menu: None,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
@@ -1648,8 +1545,9 @@ impl Editor {
key_context.set("extension", extension.to_string());
}
- if self.has_active_copilot_suggestion(cx) {
+ if self.has_active_inline_completion(cx) {
key_context.add("copilot_suggestion");
+ key_context.add("inline_completion");
}
key_context
@@ -1771,6 +1669,20 @@ impl Editor {
self.completion_provider = Some(hub);
}
+ pub fn set_inline_completion_provider(
+ &mut self,
+ provider: Model<impl InlineCompletionProvider>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.inline_completion_provider = Some(RegisteredInlineCompletionProvider {
+ _subscription: cx.observe(&provider, |this, _, cx| {
+ this.update_visible_inline_completion(cx);
+ }),
+ provider: Arc::new(provider),
+ });
+ self.refresh_inline_completion(false, cx);
+ }
+
pub fn placeholder_text(&self, _cx: &mut WindowContext) -> Option<&str> {
self.placeholder_text.as_deref()
}
@@ -1853,8 +1765,8 @@ impl Editor {
self.auto_replace_emoji_shortcode = auto_replace;
}
- pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) {
- self.show_copilot_suggestions = show_copilot_suggestions;
+ pub fn set_show_inline_completions(&mut self, show_inline_completions: bool) {
+ self.show_inline_completions = show_inline_completions;
}
pub fn set_use_modal_editing(&mut self, to: bool) {
@@ -1966,7 +1878,7 @@ impl Editor {
self.refresh_code_actions(cx);
self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, cx);
- self.discard_copilot_suggestion(cx);
+ self.discard_inline_completion(cx);
}
self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -2392,7 +2304,7 @@ impl Editor {
return true;
}
- if self.discard_copilot_suggestion(cx) {
+ if self.discard_inline_completion(cx) {
return true;
}
@@ -2647,7 +2559,7 @@ impl Editor {
}
drop(snapshot);
- let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
+ let had_active_copilot_completion = this.has_active_inline_completion(cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
if brace_inserted {
@@ -2663,14 +2575,14 @@ impl Editor {
}
}
- if had_active_copilot_suggestion {
- this.refresh_copilot_suggestions(true, cx);
- if !this.has_active_copilot_suggestion(cx) {
+ if had_active_copilot_completion {
+ this.refresh_inline_completion(true, cx);
+ if !this.has_active_inline_completion(cx) {
this.trigger_completion_on_input(&text, cx);
}
} else {
this.trigger_completion_on_input(&text, cx);
- this.refresh_copilot_suggestions(true, cx);
+ this.refresh_inline_completion(true, cx);
}
});
}
@@ -2856,7 +2768,7 @@ impl Editor {
.collect();
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
- this.refresh_copilot_suggestions(true, cx);
+ this.refresh_inline_completion(true, cx);
});
}
@@ -3503,15 +3415,15 @@ impl Editor {
let menu = menu.unwrap();
*context_menu = Some(ContextMenu::Completions(menu));
drop(context_menu);
- this.discard_copilot_suggestion(cx);
+ this.discard_inline_completion(cx);
cx.notify();
} else if this.completion_tasks.len() <= 1 {
// If there are no more completion tasks and the last menu was
// empty, we should hide it. If it was already hidden, we should
- // also show the copilot suggestion when available.
+ // also show the copilot completion when available.
drop(context_menu);
if this.hide_context_menu(cx).is_none() {
- this.update_visible_copilot_suggestion(cx);
+ this.update_visible_inline_completion(cx);
}
}
})?;
@@ -3637,7 +3549,7 @@ impl Editor {
});
}
- this.refresh_copilot_suggestions(true, cx);
+ this.refresh_inline_completion(true, cx);
});
let provider = self.completion_provider.as_ref()?;
@@ -3674,7 +3586,7 @@ impl Editor {
if this.focus_handle.is_focused(cx) {
if let Some((buffer, actions)) = this.available_code_actions.clone() {
this.completion_tasks.clear();
- this.discard_copilot_suggestion(cx);
+ this.discard_inline_completion(cx);
*this.context_menu.write() =
Some(ContextMenu::CodeActions(CodeActionsMenu {
buffer,
@@ -3949,115 +3861,55 @@ impl Editor {
None
}
- fn refresh_copilot_suggestions(
+ fn refresh_inline_completion(
&mut self,
debounce: bool,
cx: &mut ViewContext<Self>,
) -> Option<()> {
- let copilot = Copilot::global(cx)?;
- if !self.show_copilot_suggestions || !copilot.read(cx).status().is_authorized() {
- self.clear_copilot_suggestions(cx);
- return None;
- }
- self.update_visible_copilot_suggestion(cx);
-
- let snapshot = self.buffer.read(cx).snapshot(cx);
+ let provider = self.inline_completion_provider()?;
let cursor = self.selections.newest_anchor().head();
- if !self.is_copilot_enabled_at(cursor, &snapshot, cx) {
- self.clear_copilot_suggestions(cx);
+ let (buffer, cursor_buffer_position) =
+ self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+ if !self.show_inline_completions
+ || !provider.is_enabled(&buffer, cursor_buffer_position, cx)
+ {
+ self.clear_inline_completion(cx);
return None;
}
- let (buffer, buffer_position) =
- self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
- self.copilot_state.pending_refresh = cx.spawn(|this, mut cx| async move {
- if debounce {
- cx.background_executor()
- .timer(COPILOT_DEBOUNCE_TIMEOUT)
- .await;
- }
-
- let completions = copilot
- .update(&mut cx, |copilot, cx| {
- copilot.completions(&buffer, buffer_position, cx)
- })
- .log_err()
- .unwrap_or(Task::ready(Ok(Vec::new())))
- .await
- .log_err()
- .into_iter()
- .flatten()
- .collect_vec();
-
- this.update(&mut cx, |this, cx| {
- if !completions.is_empty() {
- this.copilot_state.cycled = false;
- this.copilot_state.pending_cycling_refresh = Task::ready(None);
- this.copilot_state.completions.clear();
- this.copilot_state.active_completion_index = 0;
- this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
- for completion in completions {
- this.copilot_state.push_completion(completion);
- }
- this.update_visible_copilot_suggestion(cx);
- }
- })
- .log_err()?;
- Some(())
- });
-
+ self.update_visible_inline_completion(cx);
+ provider.refresh(buffer, cursor_buffer_position, debounce, cx);
Some(())
}
- fn cycle_copilot_suggestions(
+ fn cycle_inline_completion(
&mut self,
direction: Direction,
cx: &mut ViewContext<Self>,
) -> Option<()> {
- let copilot = Copilot::global(cx)?;
- if !self.show_copilot_suggestions || !copilot.read(cx).status().is_authorized() {
+ let provider = self.inline_completion_provider()?;
+ let cursor = self.selections.newest_anchor().head();
+ let (buffer, cursor_buffer_position) =
+ self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+ if !self.show_inline_completions
+ || !provider.is_enabled(&buffer, cursor_buffer_position, cx)
+ {
return None;
}
- if self.copilot_state.cycled {
- self.copilot_state.cycle_completions(direction);
- self.update_visible_copilot_suggestion(cx);
- } else {
- let cursor = self.selections.newest_anchor().head();
- let (buffer, buffer_position) =
- self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
- self.copilot_state.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {
- let completions = copilot
- .update(&mut cx, |copilot, cx| {
- copilot.completions_cycling(&buffer, buffer_position, cx)
- })
- .log_err()?
- .await;
-
- this.update(&mut cx, |this, cx| {
- this.copilot_state.cycled = true;
- for completion in completions.log_err().into_iter().flatten() {
- this.copilot_state.push_completion(completion);
- }
- this.copilot_state.cycle_completions(direction);
- this.update_visible_copilot_suggestion(cx);
- })
- .log_err()?;
-
- Some(())
- });
- }
+ provider.cycle(buffer, cursor_buffer_position, direction, cx);
+ self.update_visible_inline_completion(cx);
Some(())
}
- fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
- if !self.has_active_copilot_suggestion(cx) {
- self.refresh_copilot_suggestions(false, cx);
+ pub fn show_inline_completion(&mut self, _: &ShowInlineCompletion, cx: &mut ViewContext<Self>) {
+ if !self.has_active_inline_completion(cx) {
+ self.refresh_inline_completion(false, cx);
return;
}
- self.update_visible_copilot_suggestion(cx);
+ self.update_visible_inline_completion(cx);
}
pub fn display_cursor_names(&mut self, _: &DisplayCursorNames, cx: &mut ViewContext<Self>) {
@@ -4078,48 +3930,43 @@ impl Editor {
.detach();
}
- fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
- if self.has_active_copilot_suggestion(cx) {
- self.cycle_copilot_suggestions(Direction::Next, cx);
+ pub fn next_inline_completion(&mut self, _: &NextInlineCompletion, cx: &mut ViewContext<Self>) {
+ if self.has_active_inline_completion(cx) {
+ self.cycle_inline_completion(Direction::Next, cx);
} else {
- let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none();
+ let is_copilot_disabled = self.refresh_inline_completion(false, cx).is_none();
if is_copilot_disabled {
cx.propagate();
}
}
}
- fn previous_copilot_suggestion(
+ pub fn previous_inline_completion(
&mut self,
- _: &copilot::PreviousSuggestion,
+ _: &PreviousInlineCompletion,
cx: &mut ViewContext<Self>,
) {
- if self.has_active_copilot_suggestion(cx) {
- self.cycle_copilot_suggestions(Direction::Prev, cx);
+ if self.has_active_inline_completion(cx) {
+ self.cycle_inline_completion(Direction::Prev, cx);
} else {
- let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none();
+ let is_copilot_disabled = self.refresh_inline_completion(false, cx).is_none();
if is_copilot_disabled {
cx.propagate();
}
}
}
- fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
- if let Some((copilot, completion)) =
- Copilot::global(cx).zip(self.copilot_state.active_completion())
- {
- copilot
- .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
- .detach_and_log_err(cx);
-
- self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
+ fn accept_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ if let Some(completion) = self.take_active_inline_completion(cx) {
+ if let Some(provider) = self.inline_completion_provider() {
+ provider.accept(cx);
}
+
cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: None,
- text: suggestion.text.to_string().into(),
+ text: completion.text.to_string().into(),
});
- self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
+ self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
cx.notify();
true
} else {
@@ -4127,21 +3974,21 @@ impl Editor {
}
}
- fn accept_partial_copilot_suggestion(
+ pub fn accept_partial_inline_completion(
&mut self,
- _: &AcceptPartialCopilotSuggestion,
+ _: &AcceptPartialInlineCompletion,
cx: &mut ViewContext<Self>,
) {
- if self.selections.count() == 1 && self.has_active_copilot_suggestion(cx) {
- if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
- let mut partial_suggestion = suggestion
+ if self.selections.count() == 1 && self.has_active_inline_completion(cx) {
+ if let Some(completion) = self.take_active_inline_completion(cx) {
+ let mut partial_completion = completion
.text
.chars()
.by_ref()
.take_while(|c| c.is_alphabetic())
.collect::<String>();
- if partial_suggestion.is_empty() {
- partial_suggestion = suggestion
+ if partial_completion.is_empty() {
+ partial_completion = completion
.text
.chars()
.by_ref()
@@ -4151,111 +3998,92 @@ impl Editor {
cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: None,
- text: partial_suggestion.clone().into(),
+ text: partial_completion.clone().into(),
});
- self.insert_with_autoindent_mode(&partial_suggestion, None, cx);
- self.refresh_copilot_suggestions(true, cx);
+ self.insert_with_autoindent_mode(&partial_completion, None, cx);
+ self.refresh_inline_completion(true, cx);
cx.notify();
}
}
}
- fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
- if let Some(copilot) = Copilot::global(cx) {
- copilot
- .update(cx, |copilot, cx| {
- copilot.discard_completions(&self.copilot_state.completions, cx)
- })
- .detach_and_log_err(cx);
-
- self.report_copilot_event(None, false, cx)
- }
-
- self.display_map.update(cx, |map, cx| {
- map.splice_inlays(vec![suggestion.id], Vec::new(), cx)
- });
- cx.notify();
- true
- } else {
- false
+ fn discard_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ if let Some(provider) = self.inline_completion_provider() {
+ provider.discard(cx);
}
- }
- fn is_copilot_enabled_at(
- &self,
- location: Anchor,
- snapshot: &MultiBufferSnapshot,
- cx: &mut ViewContext<Self>,
- ) -> bool {
- let file = snapshot.file_at(location);
- let language = snapshot.language_at(location);
- let settings = all_language_settings(file, cx);
- self.show_copilot_suggestions
- && settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
+ self.take_active_inline_completion(cx).is_some()
}
- fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
- if let Some(suggestion) = self.copilot_state.suggestion.as_ref() {
+ pub fn has_active_inline_completion(&self, cx: &AppContext) -> bool {
+ if let Some(completion) = self.active_inline_completion.as_ref() {
let buffer = self.buffer.read(cx).read(cx);
- suggestion.position.is_valid(&buffer)
+ completion.position.is_valid(&buffer)
} else {
false
}
}
- fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
- let suggestion = self.copilot_state.suggestion.take()?;
+ fn take_active_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
+ let completion = self.active_inline_completion.take()?;
self.display_map.update(cx, |map, cx| {
- map.splice_inlays(vec![suggestion.id], Default::default(), cx);
+ map.splice_inlays(vec![completion.id], Default::default(), cx);
});
let buffer = self.buffer.read(cx).read(cx);
- if suggestion.position.is_valid(&buffer) {
- Some(suggestion)
+ if completion.position.is_valid(&buffer) {
+ Some(completion)
} else {
None
}
}
- fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
- let snapshot = self.buffer.read(cx).snapshot(cx);
+ fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
let selection = self.selections.newest_anchor();
let cursor = selection.head();
- if self.context_menu.read().is_some()
- || !self.completion_tasks.is_empty()
- || selection.start != selection.end
+ if self.context_menu.read().is_none()
+ && self.completion_tasks.is_empty()
+ && selection.start == selection.end
{
- self.discard_copilot_suggestion(cx);
- } else if let Some(text) = self
- .copilot_state
- .text_for_active_completion(cursor, &snapshot)
- {
- let text = Rope::from(text);
- let mut to_remove = Vec::new();
- if let Some(suggestion) = self.copilot_state.suggestion.take() {
- to_remove.push(suggestion.id);
- }
+ if let Some(provider) = self.inline_completion_provider() {
+ if let Some((buffer, cursor_buffer_position)) =
+ self.buffer.read(cx).text_anchor_for_position(cursor, cx)
+ {
+ if let Some(text) =
+ provider.active_completion_text(&buffer, cursor_buffer_position, cx)
+ {
+ let text = Rope::from(text);
+ let mut to_remove = Vec::new();
+ if let Some(completion) = self.active_inline_completion.take() {
+ to_remove.push(completion.id);
+ }
- let suggestion_inlay =
- Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
- self.copilot_state.suggestion = Some(suggestion_inlay.clone());
- self.display_map.update(cx, move |map, cx| {
- map.splice_inlays(to_remove, vec![suggestion_inlay], cx)
- });
- cx.notify();
- } else {
- self.discard_copilot_suggestion(cx);
+ let completion_inlay =
+ Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
+ self.active_inline_completion = Some(completion_inlay.clone());
+ self.display_map.update(cx, move |map, cx| {
+ map.splice_inlays(to_remove, vec![completion_inlay], cx)
+ });
+ cx.notify();
+ return;
+ }
+ }
+ }
}
+
+ self.discard_inline_completion(cx);
}
- fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
- if let Some(old_suggestion) = self.copilot_state.suggestion.take() {
- self.splice_inlays(vec![old_suggestion.id], Vec::new(), cx);
+ fn clear_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(old_completion) = self.active_inline_completion.take() {
+ self.splice_inlays(vec![old_completion.id], Vec::new(), cx);
}
- self.copilot_state = CopilotState::default();
- self.discard_copilot_suggestion(cx);
+ self.discard_inline_completion(cx);
+ }
+
+ fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
+ Some(self.inline_completion_provider.as_ref()?.provider.clone())
}
pub fn render_code_actions_indicator(
@@ -4353,7 +4181,7 @@ impl Editor {
self.completion_tasks.clear();
let context_menu = self.context_menu.write().take();
if context_menu.is_some() {
- self.update_visible_copilot_suggestion(cx);
+ self.update_visible_inline_completion(cx);
}
context_menu
}
@@ -4546,7 +4374,7 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.insert("", cx);
- this.refresh_copilot_suggestions(true, cx);
+ this.refresh_inline_completion(true, cx);
});
}
@@ -4564,7 +4392,7 @@ impl Editor {
})
});
this.insert("", cx);
- this.refresh_copilot_suggestions(true, cx);
+ this.refresh_inline_completion(true, cx);
});
}
@@ -4626,13 +4454,13 @@ impl Editor {
}
}
- // Accept copilot suggestion if there is only one selection and the cursor is not
+ // Accept copilot completion if there is only one selection and the cursor is not
// in the leading whitespace.
if self.selections.count() == 1
&& cursor.column >= current_indent.len
- && self.has_active_copilot_suggestion(cx)
+ && self.has_active_inline_completion(cx)
{
- self.accept_copilot_suggestion(cx);
+ self.accept_inline_completion(cx);
return;
}
@@ -4659,7 +4487,7 @@ impl Editor {
self.transact(cx, |this, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
- this.refresh_copilot_suggestions(true, cx);
+ this.refresh_inline_completion(true, cx);
});
}
@@ -5753,7 +5581,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
- self.refresh_copilot_suggestions(true, cx);
+ self.refresh_inline_completion(true, cx);
cx.emit(EditorEvent::Edited);
cx.emit(EditorEvent::TransactionUndone {
transaction_id: tx_id,
@@ -5775,7 +5603,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
- self.refresh_copilot_suggestions(true, cx);
+ self.refresh_inline_completion(true, cx);
cx.emit(EditorEvent::Edited);
}
}
@@ -9444,8 +9272,8 @@ impl Editor {
} => {
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
- if self.has_active_copilot_suggestion(cx) {
- self.update_visible_copilot_suggestion(cx);
+ if self.has_active_inline_completion(cx) {
+ self.update_visible_inline_completion(cx);
}
cx.emit(EditorEvent::BufferEdited);
cx.emit(SearchEvent::MatchesInvalidated);
@@ -9523,7 +9351,7 @@ impl Editor {
}
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
- self.refresh_copilot_suggestions(true, cx);
+ self.refresh_inline_completion(true, cx);
self.refresh_inlay_hints(
InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
self.selections.newest_anchor().head(),
@@ -9687,29 +9515,6 @@ impl Editor {
.collect()
}
- fn report_copilot_event(
- &self,
- suggestion_id: Option<String>,
- suggestion_accepted: bool,
- cx: &AppContext,
- ) {
- let Some(project) = &self.project else { return };
-
- // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension
- let file_extension = self
- .buffer
- .read(cx)
- .as_singleton()
- .and_then(|b| b.read(cx).file())
- .and_then(|file| Path::new(file.file_name(cx)).extension())
- .and_then(|e| e.to_str())
- .map(|a| a.to_string());
-
- let telemetry = project.read(cx).client().telemetry().clone();
-
- telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension)
- }
-
fn report_editor_event(
&self,
operation: &'static str,
@@ -7,7 +7,6 @@ use crate::{
},
JoinLines,
};
-
use futures::StreamExt;
use gpui::{div, TestAppContext, VisualTestContext, WindowOptions};
use indoc::indoc;
@@ -7682,648 +7681,6 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
);
}
-#[gpui::test(iterations = 10)]
-async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
- // flaky
- init_test(cx, |_| {});
-
- let (copilot, copilot_lsp) = Copilot::fake(cx);
- _ = cx.update(|cx| Copilot::set_global(copilot, cx));
- 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;
-
- // When inserting, ensure autocompletion is favored over Copilot suggestions.
- cx.set_state(indoc! {"
- oneΛ
- two
- three
- "});
- cx.simulate_keystroke(".");
- let _ = handle_completion_request(
- &mut cx,
- indoc! {"
- one.|<>
- two
- three
- "},
- vec!["completion_a", "completion_b"],
- );
- handle_copilot_completion_request(
- &copilot_lsp,
- vec![copilot::request::Completion {
- text: "one.copilot1".into(),
- range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
- ..Default::default()
- }],
- vec![],
- );
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- cx.update_editor(|editor, cx| {
- assert!(editor.context_menu_visible());
- assert!(!editor.has_active_copilot_suggestion(cx));
-
- // Confirming a completion inserts it and hides the context menu, without showing
- // the copilot suggestion afterwards.
- editor
- .confirm_completion(&Default::default(), cx)
- .unwrap()
- .detach();
- assert!(!editor.context_menu_visible());
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
- assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
- });
-
- // Ensure Copilot suggestions are shown right away if no autocompletion is available.
- cx.set_state(indoc! {"
- oneΛ
- two
- three
- "});
- cx.simulate_keystroke(".");
- let _ = handle_completion_request(
- &mut cx,
- indoc! {"
- one.|<>
- two
- three
- "},
- vec![],
- );
- handle_copilot_completion_request(
- &copilot_lsp,
- vec![copilot::request::Completion {
- text: "one.copilot1".into(),
- range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
- ..Default::default()
- }],
- vec![],
- );
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- cx.update_editor(|editor, cx| {
- assert!(!editor.context_menu_visible());
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
- });
-
- // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
- cx.set_state(indoc! {"
- oneΛ
- two
- three
- "});
- cx.simulate_keystroke(".");
- let _ = handle_completion_request(
- &mut cx,
- indoc! {"
- one.|<>
- two
- three
- "},
- vec!["completion_a", "completion_b"],
- );
- handle_copilot_completion_request(
- &copilot_lsp,
- vec![copilot::request::Completion {
- text: "one.copilot1".into(),
- range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
- ..Default::default()
- }],
- vec![],
- );
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- cx.update_editor(|editor, cx| {
- assert!(editor.context_menu_visible());
- assert!(!editor.has_active_copilot_suggestion(cx));
-
- // When hiding the context menu, the Copilot suggestion becomes visible.
- editor.hide_context_menu(cx);
- assert!(!editor.context_menu_visible());
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
- });
-
- // Ensure existing completion is interpolated when inserting again.
- cx.simulate_keystroke("c");
- executor.run_until_parked();
- cx.update_editor(|editor, cx| {
- assert!(!editor.context_menu_visible());
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
- });
-
- // After debouncing, new Copilot completions should be requested.
- handle_copilot_completion_request(
- &copilot_lsp,
- vec![copilot::request::Completion {
- text: "one.copilot2".into(),
- range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
- ..Default::default()
- }],
- vec![],
- );
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- cx.update_editor(|editor, cx| {
- assert!(!editor.context_menu_visible());
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
-
- // Canceling should remove the active Copilot suggestion.
- editor.cancel(&Default::default(), cx);
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
-
- // After canceling, tabbing shouldn't insert the previously shown suggestion.
- editor.tab(&Default::default(), cx);
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
-
- // When undoing the previously active suggestion is shown again.
- editor.undo(&Default::default(), cx);
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
- });
-
- // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
- cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
- cx.update_editor(|editor, cx| {
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
-
- // Tabbing when there is an active suggestion inserts it.
- editor.tab(&Default::default(), cx);
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
-
- // When undoing the previously active suggestion is shown again.
- editor.undo(&Default::default(), cx);
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
-
- // Hide suggestion.
- editor.cancel(&Default::default(), cx);
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
- });
-
- // If an edit occurs outside of this editor but no suggestion is being shown,
- // we won't make it visible.
- cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
- cx.update_editor(|editor, cx| {
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
- assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
- });
-
- // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
- cx.update_editor(|editor, cx| {
- editor.set_text("fn foo() {\n \n}", cx);
- editor.change_selections(None, cx, |s| {
- s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
- });
- });
- handle_copilot_completion_request(
- &copilot_lsp,
- vec![copilot::request::Completion {
- text: " let x = 4;".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));
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- cx.update_editor(|editor, cx| {
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
- assert_eq!(editor.text(cx), "fn foo() {\n \n}");
-
- // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
- editor.tab(&Default::default(), cx);
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.text(cx), "fn foo() {\n \n}");
- assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
-
- // Tabbing again accepts the suggestion.
- editor.tab(&Default::default(), cx);
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
- assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
- });
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_accept_partial_copilot_suggestion(
- executor: BackgroundExecutor,
- cx: &mut gpui::TestAppContext,
-) {
- // flaky
- init_test(cx, |_| {});
-
- let (copilot, copilot_lsp) = Copilot::fake(cx);
- _ = cx.update(|cx| Copilot::set_global(copilot, cx));
- 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;
-
- // Setup the editor with a completion request.
- cx.set_state(indoc! {"
- oneΛ
- two
- three
- "});
- cx.simulate_keystroke(".");
- let _ = handle_completion_request(
- &mut cx,
- indoc! {"
- one.|<>
- two
- three
- "},
- vec![],
- );
- handle_copilot_completion_request(
- &copilot_lsp,
- vec![copilot::request::Completion {
- text: "one.copilot1".into(),
- range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
- ..Default::default()
- }],
- vec![],
- );
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- cx.update_editor(|editor, cx| {
- assert!(editor.has_active_copilot_suggestion(cx));
-
- // Accepting the first word of the suggestion should only accept the first word and still show the rest.
- editor.accept_partial_copilot_suggestion(&Default::default(), cx);
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
- assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
-
- // Accepting next word should accept the non-word and copilot suggestion should be gone
- editor.accept_partial_copilot_suggestion(&Default::default(), cx);
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
- assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
- });
-
- // Reset the editor and check non-word and whitespace completion
- cx.set_state(indoc! {"
- oneΛ
- two
- three
- "});
- cx.simulate_keystroke(".");
- let _ = handle_completion_request(
- &mut cx,
- indoc! {"
- one.|<>
- two
- three
- "},
- vec![],
- );
- handle_copilot_completion_request(
- &copilot_lsp,
- vec![copilot::request::Completion {
- text: "one.123. copilot\n 456".into(),
- range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
- ..Default::default()
- }],
- vec![],
- );
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- cx.update_editor(|editor, cx| {
- assert!(editor.has_active_copilot_suggestion(cx));
-
- // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
- editor.accept_partial_copilot_suggestion(&Default::default(), cx);
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
- assert_eq!(
- editor.display_text(cx),
- "one.123. copilot\n 456\ntwo\nthree\n"
- );
-
- // Accepting next word should accept the next word and copilot suggestion should still exist
- editor.accept_partial_copilot_suggestion(&Default::default(), cx);
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
- assert_eq!(
- editor.display_text(cx),
- "one.123. copilot\n 456\ntwo\nthree\n"
- );
-
- // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
- editor.accept_partial_copilot_suggestion(&Default::default(), cx);
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
- assert_eq!(
- editor.display_text(cx),
- "one.123. copilot\n 456\ntwo\nthree\n"
- );
- });
-}
-
-#[gpui::test]
-async fn test_copilot_completion_invalidation(
- executor: BackgroundExecutor,
- cx: &mut gpui::TestAppContext,
-) {
- init_test(cx, |_| {});
-
- let (copilot, copilot_lsp) = Copilot::fake(cx);
- _ = cx.update(|cx| Copilot::set_global(copilot, cx));
- 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));
- executor.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");
-
- // Undoing the deletion restores the suggestion.
- editor.undo(&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");
- });
-}
-
-#[gpui::test]
-async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
- init_test(cx, |_| {});
-
- let (copilot, copilot_lsp) = Copilot::fake(cx);
- _ = cx.update(|cx| Copilot::set_global(copilot, cx));
-
- let buffer_1 = cx.new_model(|cx| {
- Buffer::new(
- 0,
- BufferId::new(cx.entity_id().as_u64()).unwrap(),
- "a = 1\nb = 2\n",
- )
- });
- let buffer_2 = cx.new_model(|cx| {
- Buffer::new(
- 0,
- BufferId::new(cx.entity_id().as_u64()).unwrap(),
- "c = 3\nd = 4\n",
- )
- });
- let multibuffer = cx.new_model(|cx| {
- let mut multibuffer = MultiBuffer::new(0, ReadWrite);
- multibuffer.push_excerpts(
- buffer_1.clone(),
- [ExcerptRange {
- context: Point::new(0, 0)..Point::new(2, 0),
- primary: None,
- }],
- cx,
- );
- multibuffer.push_excerpts(
- buffer_2.clone(),
- [ExcerptRange {
- context: Point::new(0, 0)..Point::new(2, 0),
- primary: None,
- }],
- cx,
- );
- multibuffer
- });
- let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
-
- handle_copilot_completion_request(
- &copilot_lsp,
- vec![copilot::request::Completion {
- text: "b = 2 + a".into(),
- range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
- ..Default::default()
- }],
- vec![],
- );
- _ = editor.update(cx, |editor, cx| {
- // Ensure copilot suggestions are shown for the first excerpt.
- editor.change_selections(None, cx, |s| {
- s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
- });
- editor.next_copilot_suggestion(&Default::default(), cx);
- });
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- _ = editor.update(cx, |editor, cx| {
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(
- editor.display_text(cx),
- "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
- );
- assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
- });
-
- handle_copilot_completion_request(
- &copilot_lsp,
- vec![copilot::request::Completion {
- text: "d = 4 + c".into(),
- range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
- ..Default::default()
- }],
- vec![],
- );
- _ = editor.update(cx, |editor, cx| {
- // Move to another excerpt, ensuring the suggestion gets cleared.
- editor.change_selections(None, cx, |s| {
- s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
- });
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(
- editor.display_text(cx),
- "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
- );
- assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
-
- // Type a character, ensuring we don't even try to interpolate the previous suggestion.
- editor.handle_input(" ", cx);
- assert!(!editor.has_active_copilot_suggestion(cx));
- assert_eq!(
- editor.display_text(cx),
- "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
- );
- assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
- });
-
- // Ensure the new suggestion is displayed when the debounce timeout expires.
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- _ = editor.update(cx, |editor, cx| {
- assert!(editor.has_active_copilot_suggestion(cx));
- assert_eq!(
- editor.display_text(cx),
- "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
- );
- assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
- });
-}
-
-#[gpui::test]
-async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
- init_test(cx, |settings| {
- settings
- .copilot
- .get_or_insert(Default::default())
- .disabled_globs = Some(vec![".env*".to_string()]);
- });
-
- let (copilot, copilot_lsp) = Copilot::fake(cx);
- _ = cx.update(|cx| Copilot::set_global(copilot, cx));
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(
- "/test",
- json!({
- ".env": "SECRET=something\n",
- "README.md": "hello\n"
- }),
- )
- .await;
- let project = Project::test(fs, ["/test".as_ref()], cx).await;
-
- let private_buffer = project
- .update(cx, |project, cx| {
- project.open_local_buffer("/test/.env", cx)
- })
- .await
- .unwrap();
- let public_buffer = project
- .update(cx, |project, cx| {
- project.open_local_buffer("/test/README.md", cx)
- })
- .await
- .unwrap();
-
- let multibuffer = cx.new_model(|cx| {
- let mut multibuffer = MultiBuffer::new(0, ReadWrite);
- multibuffer.push_excerpts(
- private_buffer.clone(),
- [ExcerptRange {
- context: Point::new(0, 0)..Point::new(1, 0),
- primary: None,
- }],
- cx,
- );
- multibuffer.push_excerpts(
- public_buffer.clone(),
- [ExcerptRange {
- context: Point::new(0, 0)..Point::new(1, 0),
- primary: None,
- }],
- cx,
- );
- multibuffer
- });
- let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
-
- let mut copilot_requests = copilot_lsp
- .handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| async move {
- Ok(copilot::request::GetCompletionsResult {
- completions: vec![copilot::request::Completion {
- text: "next line".into(),
- range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
- ..Default::default()
- }],
- })
- });
-
- _ = editor.update(cx, |editor, cx| {
- editor.change_selections(None, cx, |selections| {
- selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
- });
- editor.next_copilot_suggestion(&Default::default(), cx);
- });
-
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- assert!(copilot_requests.try_next().is_err());
-
- _ = editor.update(cx, |editor, cx| {
- editor.change_selections(None, cx, |s| {
- s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
- });
- editor.next_copilot_suggestion(&Default::default(), cx);
- });
-
- executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
- assert!(copilot_requests.try_next().is_ok());
-}
-
#[gpui::test]
async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -9902,29 +9259,6 @@ fn handle_resolve_completion_request(
}
}
-fn handle_copilot_completion_request(
- lsp: &lsp::FakeLanguageServer,
- completions: Vec<copilot::request::Completion>,
- completions_cycling: Vec<copilot::request::Completion>,
-) {
- lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
- let completions = completions.clone();
- async move {
- Ok(copilot::request::GetCompletionsResult {
- completions: completions.clone(),
- })
- }
- });
- lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
- let completions_cycling = completions_cycling.clone();
- async move {
- Ok(copilot::request::GetCompletionsResult {
- completions: completions_cycling.clone(),
- })
- }
- });
-}
-
pub(crate) fn update_test_language_settings(
cx: &mut TestAppContext,
f: impl Fn(&mut AllLanguageSettingsContent),
@@ -344,9 +344,9 @@ impl EditorElement {
cx.propagate();
}
});
- register_action(view, cx, Editor::next_copilot_suggestion);
- register_action(view, cx, Editor::previous_copilot_suggestion);
- register_action(view, cx, Editor::copilot_suggest);
+ register_action(view, cx, Editor::next_inline_completion);
+ register_action(view, cx, Editor::previous_inline_completion);
+ register_action(view, cx, Editor::show_inline_completion);
register_action(view, cx, Editor::context_menu_first);
register_action(view, cx, Editor::context_menu_prev);
register_action(view, cx, Editor::context_menu_next);
@@ -354,7 +354,7 @@ impl EditorElement {
register_action(view, cx, Editor::display_cursor_names);
register_action(view, cx, Editor::unique_lines_case_insensitive);
register_action(view, cx, Editor::unique_lines_case_sensitive);
- register_action(view, cx, Editor::accept_partial_copilot_suggestion);
+ register_action(view, cx, Editor::accept_partial_inline_completion);
register_action(view, cx, Editor::revert_selected_hunks);
}
@@ -0,0 +1,121 @@
+use crate::Direction;
+use gpui::{AppContext, Model, ModelContext};
+use language::Buffer;
+
+pub trait InlineCompletionProvider: 'static + Sized {
+ fn is_enabled(
+ &self,
+ buffer: &Model<Buffer>,
+ cursor_position: language::Anchor,
+ cx: &AppContext,
+ ) -> bool;
+ fn refresh(
+ &mut self,
+ buffer: Model<Buffer>,
+ cursor_position: language::Anchor,
+ debounce: bool,
+ cx: &mut ModelContext<Self>,
+ );
+ fn cycle(
+ &mut self,
+ buffer: Model<Buffer>,
+ cursor_position: language::Anchor,
+ direction: Direction,
+ cx: &mut ModelContext<Self>,
+ );
+ fn accept(&mut self, cx: &mut ModelContext<Self>);
+ fn discard(&mut self, cx: &mut ModelContext<Self>);
+ fn active_completion_text(
+ &self,
+ buffer: &Model<Buffer>,
+ cursor_position: language::Anchor,
+ cx: &AppContext,
+ ) -> Option<&str>;
+}
+
+pub trait InlineCompletionProviderHandle {
+ fn is_enabled(
+ &self,
+ buffer: &Model<Buffer>,
+ cursor_position: language::Anchor,
+ cx: &AppContext,
+ ) -> bool;
+ fn refresh(
+ &self,
+ buffer: Model<Buffer>,
+ cursor_position: language::Anchor,
+ debounce: bool,
+ cx: &mut AppContext,
+ );
+ fn cycle(
+ &self,
+ buffer: Model<Buffer>,
+ cursor_position: language::Anchor,
+ direction: Direction,
+ cx: &mut AppContext,
+ );
+ fn accept(&self, cx: &mut AppContext);
+ fn discard(&self, cx: &mut AppContext);
+ fn active_completion_text<'a>(
+ &self,
+ buffer: &Model<Buffer>,
+ cursor_position: language::Anchor,
+ cx: &'a AppContext,
+ ) -> Option<&'a str>;
+}
+
+impl<T> InlineCompletionProviderHandle for Model<T>
+where
+ T: InlineCompletionProvider,
+{
+ fn is_enabled(
+ &self,
+ buffer: &Model<Buffer>,
+ cursor_position: language::Anchor,
+ cx: &AppContext,
+ ) -> bool {
+ self.read(cx).is_enabled(buffer, cursor_position, cx)
+ }
+
+ fn refresh(
+ &self,
+ buffer: Model<Buffer>,
+ cursor_position: language::Anchor,
+ debounce: bool,
+ cx: &mut AppContext,
+ ) {
+ self.update(cx, |this, cx| {
+ this.refresh(buffer, cursor_position, debounce, cx)
+ })
+ }
+
+ fn cycle(
+ &self,
+ buffer: Model<Buffer>,
+ cursor_position: language::Anchor,
+ direction: Direction,
+ cx: &mut AppContext,
+ ) {
+ self.update(cx, |this, cx| {
+ this.cycle(buffer, cursor_position, direction, cx)
+ })
+ }
+
+ fn accept(&self, cx: &mut AppContext) {
+ self.update(cx, |this, cx| this.accept(cx))
+ }
+
+ fn discard(&self, cx: &mut AppContext) {
+ self.update(cx, |this, cx| this.discard(cx))
+ }
+
+ fn active_completion_text<'a>(
+ &self,
+ buffer: &Model<Buffer>,
+ cursor_position: language::Anchor,
+ cx: &'a AppContext,
+ ) -> Option<&'a str> {
+ self.read(cx)
+ .active_completion_text(buffer, cursor_position, cx)
+ }
+}
@@ -185,7 +185,7 @@ impl FeedbackModal {
cx,
);
editor.set_show_gutter(false, cx);
- editor.set_show_copilot_suggestions(false);
+ editor.set_show_inline_completions(false);
editor.set_vertical_scroll_margin(5, cx);
editor.set_use_modal_editing(false);
editor
@@ -897,17 +897,6 @@ impl AppContext {
.unwrap()
}
- /// Updates the global of the given type with a closure. Unlike `global_mut`, this method provides
- /// your closure with mutable access to the `AppContext` and the global simultaneously.
- pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R {
- self.update(|cx| {
- let mut global = cx.lease_global::<G>();
- let result = f(&mut global, cx);
- cx.end_global_lease(global);
- result
- })
- }
-
/// Register a callback to be invoked when a global of the given type is updated.
pub fn observe_global<G: Global>(
&mut self,
@@ -1,7 +1,7 @@
use crate::{
- AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
- FocusableView, ForegroundExecutor, Global, Model, ModelContext, Render, Result, Task, View,
- ViewContext, VisualContext, WindowContext, WindowHandle,
+ AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, Context,
+ DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, Render, Result,
+ Task, View, ViewContext, VisualContext, WindowContext, WindowHandle,
};
use anyhow::{anyhow, Context as _};
use derive_more::{Deref, DerefMut};
@@ -192,7 +192,7 @@ impl AsyncAppContext {
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let mut app = app.borrow_mut();
- Ok(app.update_global(update))
+ Ok(app.update(|cx| cx.update_global(update)))
}
}
@@ -1,6 +1,6 @@
use crate::{
AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId,
- EventEmitter, Global, Model, Subscription, Task, View, WeakModel, WindowContext, WindowHandle,
+ EventEmitter, Model, Subscription, Task, View, WeakModel, WindowContext, WindowHandle,
};
use anyhow::Result;
use derive_more::{Deref, DerefMut};
@@ -190,17 +190,6 @@ impl<'a, T: 'static> ModelContext<'a, T> {
}
}
- /// Updates the given global
- pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
- where
- G: Global,
- {
- let mut global = self.app.lease_global::<G>();
- let result = f(&mut global, self);
- self.app.end_global_lease(global);
- result
- }
-
/// Spawn the future returned by the given function.
/// The function is provided a weak handle to the model owned by this context and a context that can be held across await points.
/// The returned task must be held or detached.
@@ -1,7 +1,7 @@
use crate::{
Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
- AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Empty, Entity,
- EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext,
+ AvailableSpace, BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, Empty,
+ Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext,
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
@@ -51,14 +51,6 @@ impl Context for TestAppContext {
app.update_model(handle, update)
}
- fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
- where
- F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
- {
- let mut lock = self.app.borrow_mut();
- lock.update_window(window, f)
- }
-
fn read_model<T, R>(
&self,
handle: &Model<T>,
@@ -71,6 +63,14 @@ impl Context for TestAppContext {
app.read_model(handle, read)
}
+ fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
+ where
+ F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
+ {
+ let mut lock = self.app.borrow_mut();
+ lock.update_window(window, f)
+ }
+
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
@@ -309,7 +309,7 @@ impl TestAppContext {
/// sets the global in this context.
pub fn set_global<G: Global>(&mut self, global: G) {
let mut lock = self.app.borrow_mut();
- lock.set_global(global);
+ lock.update(|cx| cx.set_global(global))
}
/// updates the global in this context. (panics if `has_global` would return false)
@@ -318,7 +318,7 @@ impl TestAppContext {
update: impl FnOnce(&mut G, &mut AppContext) -> R,
) -> R {
let mut lock = self.app.borrow_mut();
- lock.update_global(update)
+ lock.update(|cx| cx.update_global(update))
}
/// Returns an `AsyncAppContext` which can be used to run tasks that expect to be on a background
@@ -261,6 +261,10 @@ pub trait EventEmitter<E: Any>: 'static {}
pub trait BorrowAppContext {
/// Set a global value on the context.
fn set_global<T: Global>(&mut self, global: T);
+ /// Updates the global state of the given type.
+ fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
+ where
+ G: Global;
}
impl<C> BorrowAppContext for C
@@ -270,6 +274,16 @@ where
fn set_global<G: Global>(&mut self, global: G) {
self.borrow_mut().set_global(global)
}
+
+ fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
+ where
+ G: Global,
+ {
+ let mut global = self.borrow_mut().lease_global::<G>();
+ let result = f(&mut global, self);
+ self.borrow_mut().end_global_lease(global);
+ result
+ }
}
/// A flatten equivalent for anyhow `Result`s.
@@ -293,4 +307,18 @@ impl<T> Flatten<T> for Result<T> {
/// A marker trait for types that can be stored in GPUI's global state.
///
/// Implement this on types you want to store in the context as a global.
-pub trait Global: 'static {}
+pub trait Global: 'static + Sized {
+ /// Access the global of the implementing type. Panics if a global for that type has not been assigned.
+ fn get(cx: &AppContext) -> &Self {
+ cx.global()
+ }
+
+ /// Updates the global of the implementing type with a closure. Unlike `global_mut`, this method provides
+ /// your closure with mutable access to the `AppContext` and the global simultaneously.
+ fn update<C, R>(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R
+ where
+ C: BorrowAppContext,
+ {
+ cx.update_global(f)
+ }
+}
@@ -854,18 +854,6 @@ impl<'a> WindowContext<'a> {
.spawn(|app| f(AsyncWindowContext::new(app, self.window.handle)))
}
- /// Updates the global of the given type. The given closure is given simultaneous mutable
- /// access both to the global and the context.
- pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
- where
- G: Global,
- {
- let mut global = self.app.lease_global::<G>();
- let result = f(&mut global, self);
- self.app.end_global_lease(global);
- result
- }
-
fn window_bounds_changed(&mut self) {
self.window.scale_factor = self.window.platform_window.scale_factor();
self.window.viewport_size = self.window.platform_window.content_size();
@@ -2388,17 +2376,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
self.window_cx.spawn(|cx| f(view, cx))
}
- /// Updates the global state of the given type.
- pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
- where
- G: Global,
- {
- let mut global = self.app.lease_global::<G>();
- let result = f(&mut global, self);
- self.app.end_global_lease(global);
- result
- }
-
/// Register a callback to be invoked when the given global state changes.
pub fn observe_global<G: Global>(
&mut self,
@@ -6,7 +6,7 @@ use crate::Buffer;
use clock::ReplicaId;
use collections::BTreeMap;
use futures::FutureExt as _;
-use gpui::{AppContext, Model};
+use gpui::{AppContext, BorrowAppContext, Model};
use gpui::{Context, TestAppContext};
use indoc::indoc;
use proto::deserialize_operation;
@@ -456,7 +456,7 @@ impl LspLogView {
editor.set_text(log_contents, cx);
editor.move_to_end(&MoveToEnd, cx);
editor.set_read_only(true);
- editor.set_show_copilot_suggestions(false);
+ editor.set_show_inline_completions(false);
editor
});
let editor_subscription = cx.subscribe(
@@ -293,7 +293,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
#[cfg(test)]
mod tests {
- use gpui::{Context, TestAppContext};
+ use gpui::{BorrowAppContext, Context, TestAppContext};
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
use settings::SettingsStore;
use std::num::NonZeroU32;
@@ -180,7 +180,7 @@ async fn get_cached_server_binary(
#[cfg(test)]
mod tests {
- use gpui::{Context, ModelContext, TestAppContext};
+ use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
use settings::SettingsStore;
use std::num::NonZeroU32;
@@ -419,7 +419,7 @@ mod tests {
use super::*;
use crate::language;
- use gpui::{Context, Hsla, TestAppContext};
+ use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
use language::language_settings::AllLanguageSettings;
use settings::SettingsStore;
use text::BufferId;
@@ -32,8 +32,8 @@ use futures::{
};
use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{
- AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, Context, Entity, EventEmitter,
- Model, ModelContext, PromptLevel, Task, WeakModel,
+ AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
+ EventEmitter, Model, ModelContext, PromptLevel, Task, WeakModel,
};
use itertools::Itertools;
use language::{
@@ -2,7 +2,7 @@ use crate::{settings_store::SettingsStore, Settings};
use anyhow::{Context, Result};
use fs::Fs;
use futures::{channel::mpsc, StreamExt};
-use gpui::{AppContext, BackgroundExecutor};
+use gpui::{AppContext, BackgroundExecutor, BorrowAppContext};
use std::{io::ErrorKind, path::PathBuf, sync::Arc, time::Duration};
use util::{paths, ResultExt};
@@ -1,6 +1,6 @@
use anyhow::{anyhow, Context, Result};
use collections::{btree_map, hash_map, BTreeMap, HashMap};
-use gpui::{AppContext, AsyncAppContext, Global};
+use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global};
use lazy_static::lazy_static;
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
@@ -1,6 +1,8 @@
use crate::{insert::NormalBefore, Vim, VimModeSetting};
use editor::{Editor, EditorEvent};
-use gpui::{Action, AppContext, Entity, EntityId, View, ViewContext, WindowContext};
+use gpui::{
+ Action, AppContext, BorrowAppContext, Entity, EntityId, View, ViewContext, WindowContext,
+};
use settings::{Settings, SettingsStore};
pub fn init(cx: &mut AppContext) {
@@ -1,5 +1,5 @@
use editor::test::editor_test_context::ContextHandle;
-use gpui::{px, size, Context};
+use gpui::{px, size, BorrowAppContext, Context};
use indoc::indoc;
use settings::SettingsStore;
use std::{
@@ -37,6 +37,7 @@ use serde_derive::Serialize;
use settings::{update_settings_file, Settings, SettingsStore};
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
use std::{ops::Range, sync::Arc};
+use ui::BorrowAppContext;
use visual::{visual_block_motion, visual_replace};
use workspace::{self, Workspace};
@@ -4901,8 +4901,8 @@ mod tests {
};
use fs::FakeFs;
use gpui::{
- px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
- VisualTestContext,
+ px, BorrowAppContext, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView,
+ Render, TestAppContext, VisualTestContext,
};
use project::{Project, ProjectEntryId};
use serde_json::json;
@@ -7,7 +7,7 @@ use client::Client;
use clock::FakeSystemClock;
use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
use git::GITIGNORE;
-use gpui::{ModelContext, Task, TestAppContext};
+use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext};
use parking_lot::Mutex;
use postage::stream::Stream;
use pretty_assertions::assert_eq;
@@ -8,14 +8,18 @@ use backtrace::Backtrace;
use chrono::Utc;
use clap::{command, Parser};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
-use client::{parse_zed_link, Client, ClientSettings, DevServerToken, UserStore};
+use client::{
+ parse_zed_link, telemetry::Telemetry, Client, ClientSettings, DevServerToken, UserStore,
+};
use collab_ui::channel_view::ChannelView;
+use copilot::Copilot;
+use copilot_ui::CopilotCompletionProvider;
use db::kvp::KEY_VALUE_STORE;
-use editor::Editor;
+use editor::{Editor, EditorMode};
use env_logger::Builder;
use fs::RealFs;
use futures::{future, StreamExt};
-use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
+use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task, ViewContext};
use image_viewer;
use isahc::{prelude::Configurable, Request};
use language::LanguageRegistry;
@@ -176,6 +180,7 @@ fn main() {
cx,
);
assistant::init(client.clone(), cx);
+ init_inline_completion_provider(client.telemetry().clone(), cx);
extension::init(
fs.clone(),
@@ -1041,6 +1046,8 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
use std::time::Duration;
+ use gpui::BorrowAppContext;
+
let path = {
let p = Path::new("assets/icons/file_icons/file_types.json");
let Ok(full_path) = p.canonicalize() else {
@@ -1065,3 +1072,45 @@ fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
#[cfg(not(debug_assertions))]
fn watch_file_types(_fs: Arc<dyn fs::Fs>, _cx: &mut AppContext) {}
+
+fn init_inline_completion_provider(telemetry: Arc<Telemetry>, cx: &mut AppContext) {
+ if let Some(copilot) = Copilot::global(cx) {
+ cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
+ if editor.mode() == EditorMode::Full {
+ // We renamed some of these actions to not be copilot-specific, but that
+ // would have not been backwards-compatible. So here we are re-registering
+ // the actions with the old names to not break people's keymaps.
+ editor
+ .register_action(cx.listener(
+ |editor, _: &copilot::Suggest, cx: &mut ViewContext<Editor>| {
+ editor.show_inline_completion(&Default::default(), cx);
+ },
+ ))
+ .register_action(cx.listener(
+ |editor, _: &copilot::NextSuggestion, cx: &mut ViewContext<Editor>| {
+ editor.next_inline_completion(&Default::default(), cx);
+ },
+ ))
+ .register_action(cx.listener(
+ |editor, _: &copilot::PreviousSuggestion, cx: &mut ViewContext<Editor>| {
+ editor.previous_inline_completion(&Default::default(), cx);
+ },
+ ))
+ .register_action(cx.listener(
+ |editor,
+ _: &editor::actions::AcceptPartialCopilotSuggestion,
+ cx: &mut ViewContext<Editor>| {
+ editor.accept_partial_inline_completion(&Default::default(), cx);
+ },
+ ));
+
+ let provider = cx.new_model(|_| {
+ CopilotCompletionProvider::new(copilot.clone())
+ .with_telemetry(telemetry.clone())
+ });
+ editor.set_inline_completion_provider(provider, cx)
+ }
+ })
+ .detach();
+ }
+}
@@ -879,8 +879,8 @@ mod tests {
use collections::HashSet;
use editor::{scroll::Autoscroll, DisplayPoint, Editor};
use gpui::{
- actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext,
- VisualTestContext, WindowHandle,
+ actions, Action, AnyWindowHandle, AppContext, AssetSource, BorrowAppContext, Entity,
+ TestAppContext, VisualTestContext, WindowHandle,
};
use language::{LanguageMatcher, LanguageRegistry};
use project::{Project, ProjectPath, WorktreeSettings};