Introduce InlineCompletionProvider
(#9777)
This pull request introduces a new `InlineCompletionProvider` trait, which enables making `Editor` copilot-agnostic and lets us push all the copilot functionality into the `copilot_ui` module. Long-term, I would like to merge `copilot` and `copilot_ui`, but right now `project` depends on `copilot`, which makes this impossible. The reason for adding this new trait is so that we can experiment with other inline completion providers and swap them at runtime using config settings. Please, note also that we renamed some of the existing copilot actions to be more agnostic (see release notes below). We still kept the old actions bound for backwards-compatibility, but we should probably remove them at some later version. Also, as a drive-by, we added new methods to the `Global` trait that let you read or mutate a global directly, e.g.: ```rs MyGlobal::update(cx, |global, cx| { }); ``` Release Notes: - Renamed the `copilot::Suggest` action to `editor::ShowInlineCompletion` - Renamed the `copilot::NextSuggestion` action to `editor::NextInlineCompletion` - Renamed the `copilot::PreviousSuggestion` action to `editor::PreviousInlineCompletion` - Renamed the `editor::AcceptPartialCopilotSuggestion` action to `editor::AcceptPartialInlineCompletion` --------- Co-authored-by: Nathan <nathan@zed.dev> Co-authored-by: Kyle <kylek@zed.dev> Co-authored-by: Kyle Kelley <rgbkrk@gmail.com>
This commit is contained in:
parent
b8663e56a9
commit
fb6cff89d7
45 changed files with 1472 additions and 1138 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -2442,13 +2442,20 @@ name = "copilot_ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"client",
|
||||||
"copilot",
|
"copilot",
|
||||||
"editor",
|
"editor",
|
||||||
"fs",
|
"fs",
|
||||||
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"indoc",
|
||||||
"language",
|
"language",
|
||||||
|
"lsp",
|
||||||
"menu",
|
"menu",
|
||||||
|
"project",
|
||||||
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
@ -3215,7 +3222,6 @@ dependencies = [
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"convert_case 0.6.0",
|
"convert_case 0.6.0",
|
||||||
"copilot",
|
|
||||||
"ctor",
|
"ctor",
|
||||||
"db",
|
"db",
|
||||||
"emojis",
|
"emojis",
|
||||||
|
|
|
@ -140,17 +140,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full && copilot_suggestion",
|
"context": "Editor && mode == full && inline_completion",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-]": "copilot::NextSuggestion",
|
"alt-]": "editor::NextInlineCompletion",
|
||||||
"alt-[": "copilot::PreviousSuggestion",
|
"alt-[": "editor::PreviousInlineCompletion",
|
||||||
"alt-right": "editor::AcceptPartialCopilotSuggestion"
|
"alt-right": "editor::AcceptPartialInlineCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && !copilot_suggestion",
|
"context": "Editor && !inline_completion",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-\\": "copilot::Suggest"
|
"alt-\\": "editor::ShowInlineCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -182,17 +182,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full && copilot_suggestion",
|
"context": "Editor && mode == full && inline_completion",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-]": "copilot::NextSuggestion",
|
"alt-]": "editor::NextInlineCompletion",
|
||||||
"alt-[": "copilot::PreviousSuggestion",
|
"alt-[": "editor::PreviousInlineCompletion",
|
||||||
"alt-right": "editor::AcceptPartialCopilotSuggestion"
|
"alt-right": "editor::AcceptPartialInlineCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && !copilot_suggestion",
|
"context": "Editor && !inline_completion",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-\\": "copilot::Suggest"
|
"alt-\\": "editor::ShowInlineCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -510,7 +510,7 @@
|
||||||
"ctrl-[": "vim::NormalBefore",
|
"ctrl-[": "vim::NormalBefore",
|
||||||
"ctrl-x ctrl-o": "editor::ShowCompletions",
|
"ctrl-x ctrl-o": "editor::ShowCompletions",
|
||||||
"ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
|
"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-l": "editor::ToggleCodeActions", // zed specific
|
||||||
"ctrl-x ctrl-z": "editor::Cancel",
|
"ctrl-x ctrl-z": "editor::Cancel",
|
||||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||||
|
|
|
@ -12,7 +12,7 @@ use chrono::{DateTime, Local};
|
||||||
use client::{proto, Client};
|
use client::{proto, Client};
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
pub(crate) use completion_provider::*;
|
pub(crate) use completion_provider::*;
|
||||||
use gpui::{actions, AppContext, Global, SharedString};
|
use gpui::{actions, AppContext, BorrowAppContext, Global, SharedString};
|
||||||
pub(crate) use saved_conversation::*;
|
pub(crate) use saved_conversation::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
|
|
@ -383,7 +383,7 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::AppContext;
|
use gpui::{AppContext, BorrowAppContext};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -15,7 +15,7 @@ use crate::{
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use futures::{future::BoxFuture, stream::BoxStream};
|
use futures::{future::BoxFuture, stream::BoxStream};
|
||||||
use gpui::{AnyView, AppContext, Task, WindowContext};
|
use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use assets::SoundRegistry;
|
use assets::SoundRegistry;
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
use gpui::{AppContext, AssetSource, Global};
|
use gpui::{AppContext, AssetSource, BorrowAppContext, Global};
|
||||||
use rodio::{OutputStream, OutputStreamHandle};
|
use rodio::{OutputStream, OutputStreamHandle};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@ use futures::{
|
||||||
TryFutureExt as _, TryStreamExt,
|
TryFutureExt as _, TryStreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{
|
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 lazy_static::lazy_static;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
|
@ -12,7 +12,7 @@ use editor::{
|
||||||
Editor,
|
Editor,
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{TestAppContext, VisualContext, VisualTestContext};
|
use gpui::{BorrowAppContext, TestAppContext, VisualContext, VisualTestContext};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, InlayHintSettings},
|
language_settings::{AllLanguageSettings, InlayHintSettings},
|
||||||
|
|
|
@ -7,8 +7,8 @@ use collab_ui::{
|
||||||
};
|
};
|
||||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
point, BackgroundExecutor, Context, Entity, SharedString, TestAppContext, View, VisualContext,
|
point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
|
||||||
VisualTestContext,
|
View, VisualContext, VisualTestContext,
|
||||||
};
|
};
|
||||||
use language::Capability;
|
use language::Capability;
|
||||||
use live_kit_client::MacOSDisplay;
|
use live_kit_client::MacOSDisplay;
|
||||||
|
|
|
@ -8,8 +8,8 @@ use collections::{HashMap, HashSet};
|
||||||
use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
|
use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
|
px, size, AppContext, BackgroundExecutor, BorrowAppContext, Model, Modifiers, MouseButton,
|
||||||
TestAppContext,
|
MouseDownEvent, TestAppContext,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, Formatter},
|
language_settings::{AllLanguageSettings, Formatter},
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::any::TypeId;
|
||||||
|
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
use gpui::{Action, AppContext, Global};
|
use gpui::{Action, AppContext, BorrowAppContext, Global};
|
||||||
|
|
||||||
/// Initializes the command palette hooks.
|
/// Initializes the command palette hooks.
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
client.workspace = true
|
||||||
copilot.workspace = true
|
copilot.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
|
@ -27,4 +28,11 @@ workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
copilot = { workspace = true, features = ["test-support"] }
|
||||||
editor = { 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"] }
|
||||||
|
|
1021
crates/copilot_ui/src/copilot_completion_provider.rs
Normal file
1021
crates/copilot_ui/src/copilot_completion_provider.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,7 @@
|
||||||
pub mod copilot_button;
|
pub mod copilot_button;
|
||||||
|
mod copilot_completion_provider;
|
||||||
mod sign_in;
|
mod sign_in;
|
||||||
|
|
||||||
pub use copilot_button::*;
|
pub use copilot_button::*;
|
||||||
|
pub use copilot_completion_provider::*;
|
||||||
pub use sign_in::*;
|
pub use sign_in::*;
|
||||||
|
|
|
@ -14,7 +14,6 @@ doctest = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = [
|
test-support = [
|
||||||
"copilot/test-support",
|
|
||||||
"text/test-support",
|
"text/test-support",
|
||||||
"language/test-support",
|
"language/test-support",
|
||||||
"gpui/test-support",
|
"gpui/test-support",
|
||||||
|
@ -34,7 +33,6 @@ client.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
convert_case = "0.6.0"
|
convert_case = "0.6.0"
|
||||||
copilot.workspace = true
|
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
emojis.workspace = true
|
emojis.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
@ -73,7 +71,6 @@ util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
copilot = { workspace = true, features = ["test-support"] }
|
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -127,6 +127,7 @@ gpui::actions!(
|
||||||
editor,
|
editor,
|
||||||
[
|
[
|
||||||
AcceptPartialCopilotSuggestion,
|
AcceptPartialCopilotSuggestion,
|
||||||
|
AcceptPartialInlineCompletion,
|
||||||
AddSelectionAbove,
|
AddSelectionAbove,
|
||||||
AddSelectionBelow,
|
AddSelectionBelow,
|
||||||
Backspace,
|
Backspace,
|
||||||
|
@ -168,13 +169,12 @@ gpui::actions!(
|
||||||
GoToDefinitionSplit,
|
GoToDefinitionSplit,
|
||||||
GoToDiagnostic,
|
GoToDiagnostic,
|
||||||
GoToHunk,
|
GoToHunk,
|
||||||
|
GoToImplementation,
|
||||||
|
GoToImplementationSplit,
|
||||||
GoToPrevDiagnostic,
|
GoToPrevDiagnostic,
|
||||||
GoToPrevHunk,
|
GoToPrevHunk,
|
||||||
GoToTypeDefinition,
|
GoToTypeDefinition,
|
||||||
GoToTypeDefinitionSplit,
|
GoToTypeDefinitionSplit,
|
||||||
GoToImplementation,
|
|
||||||
GoToImplementationSplit,
|
|
||||||
OpenUrl,
|
|
||||||
HalfPageDown,
|
HalfPageDown,
|
||||||
HalfPageUp,
|
HalfPageUp,
|
||||||
Hover,
|
Hover,
|
||||||
|
@ -202,21 +202,24 @@ gpui::actions!(
|
||||||
Newline,
|
Newline,
|
||||||
NewlineAbove,
|
NewlineAbove,
|
||||||
NewlineBelow,
|
NewlineBelow,
|
||||||
|
NextInlineCompletion,
|
||||||
NextScreen,
|
NextScreen,
|
||||||
OpenExcerpts,
|
OpenExcerpts,
|
||||||
OpenExcerptsSplit,
|
OpenExcerptsSplit,
|
||||||
OpenPermalinkToLine,
|
OpenPermalinkToLine,
|
||||||
|
OpenUrl,
|
||||||
Outdent,
|
Outdent,
|
||||||
PageDown,
|
PageDown,
|
||||||
PageUp,
|
PageUp,
|
||||||
Paste,
|
Paste,
|
||||||
RevertSelectedHunks,
|
PreviousInlineCompletion,
|
||||||
Redo,
|
Redo,
|
||||||
RedoSelection,
|
RedoSelection,
|
||||||
Rename,
|
Rename,
|
||||||
RestartLanguageServer,
|
RestartLanguageServer,
|
||||||
RevealInFinder,
|
RevealInFinder,
|
||||||
ReverseLines,
|
ReverseLines,
|
||||||
|
RevertSelectedHunks,
|
||||||
ScrollCursorBottom,
|
ScrollCursorBottom,
|
||||||
ScrollCursorCenter,
|
ScrollCursorCenter,
|
||||||
ScrollCursorTop,
|
ScrollCursorTop,
|
||||||
|
@ -239,6 +242,7 @@ gpui::actions!(
|
||||||
SelectUp,
|
SelectUp,
|
||||||
ShowCharacterPalette,
|
ShowCharacterPalette,
|
||||||
ShowCompletions,
|
ShowCompletions,
|
||||||
|
ShowInlineCompletion,
|
||||||
ShuffleLines,
|
ShuffleLines,
|
||||||
SortLinesCaseInsensitive,
|
SortLinesCaseInsensitive,
|
||||||
SortLinesCaseSensitive,
|
SortLinesCaseSensitive,
|
||||||
|
@ -246,8 +250,8 @@ gpui::actions!(
|
||||||
Tab,
|
Tab,
|
||||||
TabPrev,
|
TabPrev,
|
||||||
ToggleInlayHints,
|
ToggleInlayHints,
|
||||||
ToggleSoftWrap,
|
|
||||||
ToggleLineNumbers,
|
ToggleLineNumbers,
|
||||||
|
ToggleSoftWrap,
|
||||||
Transpose,
|
Transpose,
|
||||||
Undo,
|
Undo,
|
||||||
UndoSelection,
|
UndoSelection,
|
||||||
|
|
|
@ -1015,7 +1015,7 @@ pub mod tests {
|
||||||
movement,
|
movement,
|
||||||
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
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::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||||
Buffer, Language, LanguageConfig, LanguageMatcher, SelectionGoal,
|
Buffer, Language, LanguageConfig, LanguageMatcher, SelectionGoal,
|
||||||
|
|
|
@ -24,6 +24,7 @@ mod git;
|
||||||
mod highlight_matching_bracket;
|
mod highlight_matching_bracket;
|
||||||
mod hover_links;
|
mod hover_links;
|
||||||
mod hover_popover;
|
mod hover_popover;
|
||||||
|
mod inline_completion_provider;
|
||||||
pub mod items;
|
pub mod items;
|
||||||
mod mouse_context_menu;
|
mod mouse_context_menu;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
|
@ -45,7 +46,6 @@ use client::{Collaborator, ParticipantIndex};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
use collections::{hash_map, BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use copilot::Copilot;
|
|
||||||
use debounced_delay::DebouncedDelay;
|
use debounced_delay::DebouncedDelay;
|
||||||
pub use display_map::DisplayPoint;
|
pub use display_map::DisplayPoint;
|
||||||
use display_map::*;
|
use display_map::*;
|
||||||
|
@ -69,6 +69,7 @@ use gpui::{
|
||||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||||
use hover_popover::{hide_hover, HoverState};
|
use hover_popover::{hide_hover, HoverState};
|
||||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||||
|
pub use inline_completion_provider::*;
|
||||||
pub use items::MAX_TAB_TITLE_LEN;
|
pub use items::MAX_TAB_TITLE_LEN;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{char_kind, CharKind};
|
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 MAX_LINE_LEN: usize = 1024;
|
||||||
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
||||||
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
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);
|
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
|
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
|
||||||
|
@ -419,7 +419,9 @@ pub struct Editor {
|
||||||
hover_state: HoverState,
|
hover_state: HoverState,
|
||||||
gutter_hovered: bool,
|
gutter_hovered: bool,
|
||||||
hovered_link_state: Option<HoveredLinkState>,
|
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,
|
inlay_hint_cache: InlayHintCache,
|
||||||
next_inlay_id: usize,
|
next_inlay_id: usize,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
@ -428,7 +430,6 @@ pub struct Editor {
|
||||||
pub vim_replace_map: HashMap<Range<usize>, String>,
|
pub vim_replace_map: HashMap<Range<usize>, String>,
|
||||||
style: Option<EditorStyle>,
|
style: Option<EditorStyle>,
|
||||||
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
|
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
|
||||||
show_copilot_suggestions: bool,
|
|
||||||
use_autoclose: bool,
|
use_autoclose: bool,
|
||||||
auto_replace_emoji_shortcode: bool,
|
auto_replace_emoji_shortcode: bool,
|
||||||
custom_context_menu: Option<
|
custom_context_menu: Option<
|
||||||
|
@ -625,6 +626,11 @@ pub struct RenameState {
|
||||||
|
|
||||||
struct InvalidationStack<T>(Vec<T>);
|
struct InvalidationStack<T>(Vec<T>);
|
||||||
|
|
||||||
|
struct RegisteredInlineCompletionProvider {
|
||||||
|
provider: Arc<dyn InlineCompletionProviderHandle>,
|
||||||
|
_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
enum ContextMenu {
|
enum ContextMenu {
|
||||||
Completions(CompletionsMenu),
|
Completions(CompletionsMenu),
|
||||||
CodeActions(CodeActionsMenu),
|
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)]
|
#[derive(Debug)]
|
||||||
struct ActiveDiagnosticGroup {
|
struct ActiveDiagnosticGroup {
|
||||||
primary_range: Range<Anchor>,
|
primary_range: Range<Anchor>,
|
||||||
|
@ -1562,7 +1458,8 @@ impl Editor {
|
||||||
remote_id: None,
|
remote_id: None,
|
||||||
hover_state: Default::default(),
|
hover_state: Default::default(),
|
||||||
hovered_link_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),
|
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
|
||||||
gutter_hovered: false,
|
gutter_hovered: false,
|
||||||
pixel_position_of_newest_cursor: None,
|
pixel_position_of_newest_cursor: None,
|
||||||
|
@ -1572,7 +1469,7 @@ impl Editor {
|
||||||
hovered_cursors: Default::default(),
|
hovered_cursors: Default::default(),
|
||||||
editor_actions: Default::default(),
|
editor_actions: Default::default(),
|
||||||
vim_replace_map: Default::default(),
|
vim_replace_map: Default::default(),
|
||||||
show_copilot_suggestions: mode == EditorMode::Full,
|
show_inline_completions: mode == EditorMode::Full,
|
||||||
custom_context_menu: None,
|
custom_context_menu: None,
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
cx.observe(&buffer, Self::on_buffer_changed),
|
cx.observe(&buffer, Self::on_buffer_changed),
|
||||||
|
@ -1648,8 +1545,9 @@ impl Editor {
|
||||||
key_context.set("extension", extension.to_string());
|
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("copilot_suggestion");
|
||||||
|
key_context.add("inline_completion");
|
||||||
}
|
}
|
||||||
|
|
||||||
key_context
|
key_context
|
||||||
|
@ -1771,6 +1669,20 @@ impl Editor {
|
||||||
self.completion_provider = Some(hub);
|
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> {
|
pub fn placeholder_text(&self, _cx: &mut WindowContext) -> Option<&str> {
|
||||||
self.placeholder_text.as_deref()
|
self.placeholder_text.as_deref()
|
||||||
}
|
}
|
||||||
|
@ -1853,8 +1765,8 @@ impl Editor {
|
||||||
self.auto_replace_emoji_shortcode = auto_replace;
|
self.auto_replace_emoji_shortcode = auto_replace;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) {
|
pub fn set_show_inline_completions(&mut self, show_inline_completions: bool) {
|
||||||
self.show_copilot_suggestions = show_copilot_suggestions;
|
self.show_inline_completions = show_inline_completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_use_modal_editing(&mut self, to: bool) {
|
pub fn set_use_modal_editing(&mut self, to: bool) {
|
||||||
|
@ -1966,7 +1878,7 @@ impl Editor {
|
||||||
self.refresh_code_actions(cx);
|
self.refresh_code_actions(cx);
|
||||||
self.refresh_document_highlights(cx);
|
self.refresh_document_highlights(cx);
|
||||||
refresh_matching_bracket_highlights(self, cx);
|
refresh_matching_bracket_highlights(self, cx);
|
||||||
self.discard_copilot_suggestion(cx);
|
self.discard_inline_completion(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||||
|
@ -2392,7 +2304,7 @@ impl Editor {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.discard_copilot_suggestion(cx) {
|
if self.discard_inline_completion(cx) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2647,7 +2559,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(snapshot);
|
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));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||||
|
|
||||||
if brace_inserted {
|
if brace_inserted {
|
||||||
|
@ -2663,14 +2575,14 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if had_active_copilot_suggestion {
|
if had_active_copilot_completion {
|
||||||
this.refresh_copilot_suggestions(true, cx);
|
this.refresh_inline_completion(true, cx);
|
||||||
if !this.has_active_copilot_suggestion(cx) {
|
if !this.has_active_inline_completion(cx) {
|
||||||
this.trigger_completion_on_input(&text, cx);
|
this.trigger_completion_on_input(&text, cx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.trigger_completion_on_input(&text, cx);
|
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();
|
.collect();
|
||||||
|
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
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();
|
let menu = menu.unwrap();
|
||||||
*context_menu = Some(ContextMenu::Completions(menu));
|
*context_menu = Some(ContextMenu::Completions(menu));
|
||||||
drop(context_menu);
|
drop(context_menu);
|
||||||
this.discard_copilot_suggestion(cx);
|
this.discard_inline_completion(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else if this.completion_tasks.len() <= 1 {
|
} else if this.completion_tasks.len() <= 1 {
|
||||||
// If there are no more completion tasks and the last menu was
|
// If there are no more completion tasks and the last menu was
|
||||||
// empty, we should hide it. If it was already hidden, we should
|
// 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);
|
drop(context_menu);
|
||||||
if this.hide_context_menu(cx).is_none() {
|
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()?;
|
let provider = self.completion_provider.as_ref()?;
|
||||||
|
@ -3674,7 +3586,7 @@ impl Editor {
|
||||||
if this.focus_handle.is_focused(cx) {
|
if this.focus_handle.is_focused(cx) {
|
||||||
if let Some((buffer, actions)) = this.available_code_actions.clone() {
|
if let Some((buffer, actions)) = this.available_code_actions.clone() {
|
||||||
this.completion_tasks.clear();
|
this.completion_tasks.clear();
|
||||||
this.discard_copilot_suggestion(cx);
|
this.discard_inline_completion(cx);
|
||||||
*this.context_menu.write() =
|
*this.context_menu.write() =
|
||||||
Some(ContextMenu::CodeActions(CodeActionsMenu {
|
Some(ContextMenu::CodeActions(CodeActionsMenu {
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -3949,115 +3861,55 @@ impl Editor {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_copilot_suggestions(
|
fn refresh_inline_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
debounce: bool,
|
debounce: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let copilot = Copilot::global(cx)?;
|
let provider = self.inline_completion_provider()?;
|
||||||
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 cursor = self.selections.newest_anchor().head();
|
let cursor = self.selections.newest_anchor().head();
|
||||||
if !self.is_copilot_enabled_at(cursor, &snapshot, cx) {
|
let (buffer, cursor_buffer_position) =
|
||||||
self.clear_copilot_suggestions(cx);
|
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;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (buffer, buffer_position) =
|
self.update_visible_inline_completion(cx);
|
||||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
provider.refresh(buffer, cursor_buffer_position, debounce, 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(())
|
|
||||||
});
|
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cycle_copilot_suggestions(
|
fn cycle_inline_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let copilot = Copilot::global(cx)?;
|
let provider = self.inline_completion_provider()?;
|
||||||
if !self.show_copilot_suggestions || !copilot.read(cx).status().is_authorized() {
|
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;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.copilot_state.cycled {
|
provider.cycle(buffer, cursor_buffer_position, direction, cx);
|
||||||
self.copilot_state.cycle_completions(direction);
|
self.update_visible_inline_completion(cx);
|
||||||
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(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
|
pub fn show_inline_completion(&mut self, _: &ShowInlineCompletion, cx: &mut ViewContext<Self>) {
|
||||||
if !self.has_active_copilot_suggestion(cx) {
|
if !self.has_active_inline_completion(cx) {
|
||||||
self.refresh_copilot_suggestions(false, cx);
|
self.refresh_inline_completion(false, cx);
|
||||||
return;
|
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>) {
|
pub fn display_cursor_names(&mut self, _: &DisplayCursorNames, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -4078,48 +3930,43 @@ impl Editor {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
pub fn next_inline_completion(&mut self, _: &NextInlineCompletion, cx: &mut ViewContext<Self>) {
|
||||||
if self.has_active_copilot_suggestion(cx) {
|
if self.has_active_inline_completion(cx) {
|
||||||
self.cycle_copilot_suggestions(Direction::Next, cx);
|
self.cycle_inline_completion(Direction::Next, cx);
|
||||||
} else {
|
} 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 {
|
if is_copilot_disabled {
|
||||||
cx.propagate();
|
cx.propagate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous_copilot_suggestion(
|
pub fn previous_inline_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &copilot::PreviousSuggestion,
|
_: &PreviousInlineCompletion,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if self.has_active_copilot_suggestion(cx) {
|
if self.has_active_inline_completion(cx) {
|
||||||
self.cycle_copilot_suggestions(Direction::Prev, cx);
|
self.cycle_inline_completion(Direction::Prev, cx);
|
||||||
} else {
|
} 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 {
|
if is_copilot_disabled {
|
||||||
cx.propagate();
|
cx.propagate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
fn accept_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
|
if let Some(completion) = self.take_active_inline_completion(cx) {
|
||||||
if let Some((copilot, completion)) =
|
if let Some(provider) = self.inline_completion_provider() {
|
||||||
Copilot::global(cx).zip(self.copilot_state.active_completion())
|
provider.accept(cx);
|
||||||
{
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(EditorEvent::InputHandled {
|
cx.emit(EditorEvent::InputHandled {
|
||||||
utf16_range_to_replace: None,
|
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();
|
cx.notify();
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
@ -4127,21 +3974,21 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_partial_copilot_suggestion(
|
pub fn accept_partial_inline_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &AcceptPartialCopilotSuggestion,
|
_: &AcceptPartialInlineCompletion,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if self.selections.count() == 1 && self.has_active_copilot_suggestion(cx) {
|
if self.selections.count() == 1 && self.has_active_inline_completion(cx) {
|
||||||
if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
|
if let Some(completion) = self.take_active_inline_completion(cx) {
|
||||||
let mut partial_suggestion = suggestion
|
let mut partial_completion = completion
|
||||||
.text
|
.text
|
||||||
.chars()
|
.chars()
|
||||||
.by_ref()
|
.by_ref()
|
||||||
.take_while(|c| c.is_alphabetic())
|
.take_while(|c| c.is_alphabetic())
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
if partial_suggestion.is_empty() {
|
if partial_completion.is_empty() {
|
||||||
partial_suggestion = suggestion
|
partial_completion = completion
|
||||||
.text
|
.text
|
||||||
.chars()
|
.chars()
|
||||||
.by_ref()
|
.by_ref()
|
||||||
|
@ -4151,111 +3998,92 @@ impl Editor {
|
||||||
|
|
||||||
cx.emit(EditorEvent::InputHandled {
|
cx.emit(EditorEvent::InputHandled {
|
||||||
utf16_range_to_replace: None,
|
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.insert_with_autoindent_mode(&partial_completion, None, cx);
|
||||||
self.refresh_copilot_suggestions(true, cx);
|
self.refresh_inline_completion(true, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
fn discard_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
|
if let Some(provider) = self.inline_completion_provider() {
|
||||||
if let Some(copilot) = Copilot::global(cx) {
|
provider.discard(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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.take_active_inline_completion(cx).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_copilot_enabled_at(
|
pub fn has_active_inline_completion(&self, cx: &AppContext) -> bool {
|
||||||
&self,
|
if let Some(completion) = self.active_inline_completion.as_ref() {
|
||||||
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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
|
|
||||||
if let Some(suggestion) = self.copilot_state.suggestion.as_ref() {
|
|
||||||
let buffer = self.buffer.read(cx).read(cx);
|
let buffer = self.buffer.read(cx).read(cx);
|
||||||
suggestion.position.is_valid(&buffer)
|
completion.position.is_valid(&buffer)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
|
fn take_active_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
|
||||||
let suggestion = self.copilot_state.suggestion.take()?;
|
let completion = self.active_inline_completion.take()?;
|
||||||
self.display_map.update(cx, |map, cx| {
|
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);
|
let buffer = self.buffer.read(cx).read(cx);
|
||||||
|
|
||||||
if suggestion.position.is_valid(&buffer) {
|
if completion.position.is_valid(&buffer) {
|
||||||
Some(suggestion)
|
Some(completion)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
|
fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
|
||||||
let selection = self.selections.newest_anchor();
|
let selection = self.selections.newest_anchor();
|
||||||
let cursor = selection.head();
|
let cursor = selection.head();
|
||||||
|
|
||||||
if self.context_menu.read().is_some()
|
if self.context_menu.read().is_none()
|
||||||
|| !self.completion_tasks.is_empty()
|
&& self.completion_tasks.is_empty()
|
||||||
|| selection.start != selection.end
|
&& selection.start == selection.end
|
||||||
{
|
{
|
||||||
self.discard_copilot_suggestion(cx);
|
if let Some(provider) = self.inline_completion_provider() {
|
||||||
} else if let Some(text) = self
|
if let Some((buffer, cursor_buffer_position)) =
|
||||||
.copilot_state
|
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
|
||||||
.text_for_active_completion(cursor, &snapshot)
|
{
|
||||||
{
|
if let Some(text) =
|
||||||
let text = Rope::from(text);
|
provider.active_completion_text(&buffer, cursor_buffer_position, cx)
|
||||||
let mut to_remove = Vec::new();
|
{
|
||||||
if let Some(suggestion) = self.copilot_state.suggestion.take() {
|
let text = Rope::from(text);
|
||||||
to_remove.push(suggestion.id);
|
let mut to_remove = Vec::new();
|
||||||
}
|
if let Some(completion) = self.active_inline_completion.take() {
|
||||||
|
to_remove.push(completion.id);
|
||||||
|
}
|
||||||
|
|
||||||
let suggestion_inlay =
|
let completion_inlay =
|
||||||
Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
|
Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
|
||||||
self.copilot_state.suggestion = Some(suggestion_inlay.clone());
|
self.active_inline_completion = Some(completion_inlay.clone());
|
||||||
self.display_map.update(cx, move |map, cx| {
|
self.display_map.update(cx, move |map, cx| {
|
||||||
map.splice_inlays(to_remove, vec![suggestion_inlay], cx)
|
map.splice_inlays(to_remove, vec![completion_inlay], cx)
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else {
|
return;
|
||||||
self.discard_copilot_suggestion(cx);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.discard_inline_completion(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
|
fn clear_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(old_suggestion) = self.copilot_state.suggestion.take() {
|
if let Some(old_completion) = self.active_inline_completion.take() {
|
||||||
self.splice_inlays(vec![old_suggestion.id], Vec::new(), cx);
|
self.splice_inlays(vec![old_completion.id], Vec::new(), cx);
|
||||||
}
|
}
|
||||||
self.copilot_state = CopilotState::default();
|
self.discard_inline_completion(cx);
|
||||||
self.discard_copilot_suggestion(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(
|
pub fn render_code_actions_indicator(
|
||||||
|
@ -4353,7 +4181,7 @@ impl Editor {
|
||||||
self.completion_tasks.clear();
|
self.completion_tasks.clear();
|
||||||
let context_menu = self.context_menu.write().take();
|
let context_menu = self.context_menu.write().take();
|
||||||
if context_menu.is_some() {
|
if context_menu.is_some() {
|
||||||
self.update_visible_copilot_suggestion(cx);
|
self.update_visible_inline_completion(cx);
|
||||||
}
|
}
|
||||||
context_menu
|
context_menu
|
||||||
}
|
}
|
||||||
|
@ -4546,7 +4374,7 @@ impl Editor {
|
||||||
|
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||||
this.insert("", cx);
|
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.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.
|
// in the leading whitespace.
|
||||||
if self.selections.count() == 1
|
if self.selections.count() == 1
|
||||||
&& cursor.column >= current_indent.len
|
&& 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4659,7 +4487,7 @@ impl Editor {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
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.request_autoscroll(Autoscroll::fit(), cx);
|
||||||
self.unmark_text(cx);
|
self.unmark_text(cx);
|
||||||
self.refresh_copilot_suggestions(true, cx);
|
self.refresh_inline_completion(true, cx);
|
||||||
cx.emit(EditorEvent::Edited);
|
cx.emit(EditorEvent::Edited);
|
||||||
cx.emit(EditorEvent::TransactionUndone {
|
cx.emit(EditorEvent::TransactionUndone {
|
||||||
transaction_id: tx_id,
|
transaction_id: tx_id,
|
||||||
|
@ -5775,7 +5603,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||||
self.unmark_text(cx);
|
self.unmark_text(cx);
|
||||||
self.refresh_copilot_suggestions(true, cx);
|
self.refresh_inline_completion(true, cx);
|
||||||
cx.emit(EditorEvent::Edited);
|
cx.emit(EditorEvent::Edited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9444,8 +9272,8 @@ impl Editor {
|
||||||
} => {
|
} => {
|
||||||
self.refresh_active_diagnostics(cx);
|
self.refresh_active_diagnostics(cx);
|
||||||
self.refresh_code_actions(cx);
|
self.refresh_code_actions(cx);
|
||||||
if self.has_active_copilot_suggestion(cx) {
|
if self.has_active_inline_completion(cx) {
|
||||||
self.update_visible_copilot_suggestion(cx);
|
self.update_visible_inline_completion(cx);
|
||||||
}
|
}
|
||||||
cx.emit(EditorEvent::BufferEdited);
|
cx.emit(EditorEvent::BufferEdited);
|
||||||
cx.emit(SearchEvent::MatchesInvalidated);
|
cx.emit(SearchEvent::MatchesInvalidated);
|
||||||
|
@ -9523,7 +9351,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
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(
|
self.refresh_inlay_hints(
|
||||||
InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
|
InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
|
||||||
self.selections.newest_anchor().head(),
|
self.selections.newest_anchor().head(),
|
||||||
|
@ -9687,29 +9515,6 @@ impl Editor {
|
||||||
.collect()
|
.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(
|
fn report_editor_event(
|
||||||
&self,
|
&self,
|
||||||
operation: &'static str,
|
operation: &'static str,
|
||||||
|
|
|
@ -7,7 +7,6 @@ use crate::{
|
||||||
},
|
},
|
||||||
JoinLines,
|
JoinLines,
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{div, TestAppContext, VisualTestContext, WindowOptions};
|
use gpui::{div, TestAppContext, VisualTestContext, WindowOptions};
|
||||||
use indoc::indoc;
|
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]
|
#[gpui::test]
|
||||||
async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
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(
|
pub(crate) fn update_test_language_settings(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
f: impl Fn(&mut AllLanguageSettingsContent),
|
f: impl Fn(&mut AllLanguageSettingsContent),
|
||||||
|
|
|
@ -344,9 +344,9 @@ impl EditorElement {
|
||||||
cx.propagate();
|
cx.propagate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
register_action(view, cx, Editor::next_copilot_suggestion);
|
register_action(view, cx, Editor::next_inline_completion);
|
||||||
register_action(view, cx, Editor::previous_copilot_suggestion);
|
register_action(view, cx, Editor::previous_inline_completion);
|
||||||
register_action(view, cx, Editor::copilot_suggest);
|
register_action(view, cx, Editor::show_inline_completion);
|
||||||
register_action(view, cx, Editor::context_menu_first);
|
register_action(view, cx, Editor::context_menu_first);
|
||||||
register_action(view, cx, Editor::context_menu_prev);
|
register_action(view, cx, Editor::context_menu_prev);
|
||||||
register_action(view, cx, Editor::context_menu_next);
|
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::display_cursor_names);
|
||||||
register_action(view, cx, Editor::unique_lines_case_insensitive);
|
register_action(view, cx, Editor::unique_lines_case_insensitive);
|
||||||
register_action(view, cx, Editor::unique_lines_case_sensitive);
|
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);
|
register_action(view, cx, Editor::revert_selected_hunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
121
crates/editor/src/inline_completion_provider.rs
Normal file
121
crates/editor/src/inline_completion_provider.rs
Normal file
|
@ -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,
|
cx,
|
||||||
);
|
);
|
||||||
editor.set_show_gutter(false, 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_vertical_scroll_margin(5, cx);
|
||||||
editor.set_use_modal_editing(false);
|
editor.set_use_modal_editing(false);
|
||||||
editor
|
editor
|
||||||
|
|
|
@ -897,17 +897,6 @@ impl AppContext {
|
||||||
.unwrap()
|
.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.
|
/// Register a callback to be invoked when a global of the given type is updated.
|
||||||
pub fn observe_global<G: Global>(
|
pub fn observe_global<G: Global>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
|
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, Context,
|
||||||
FocusableView, ForegroundExecutor, Global, Model, ModelContext, Render, Result, Task, View,
|
DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, Render, Result,
|
||||||
ViewContext, VisualContext, WindowContext, WindowHandle,
|
Task, View, ViewContext, VisualContext, WindowContext, WindowHandle,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::{anyhow, Context as _};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
|
@ -192,7 +192,7 @@ impl AsyncAppContext {
|
||||||
.upgrade()
|
.upgrade()
|
||||||
.ok_or_else(|| anyhow!("app was released"))?;
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
let mut app = app.borrow_mut();
|
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::{
|
use crate::{
|
||||||
AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId,
|
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 anyhow::Result;
|
||||||
use derive_more::{Deref, DerefMut};
|
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.
|
/// 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 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.
|
/// The returned task must be held or detached.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||||
AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Empty, Entity,
|
AvailableSpace, BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, Empty,
|
||||||
EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext,
|
Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext,
|
||||||
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||||
Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
|
Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
|
||||||
TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
||||||
|
@ -51,14 +51,6 @@ impl Context for TestAppContext {
|
||||||
app.update_model(handle, update)
|
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>(
|
fn read_model<T, R>(
|
||||||
&self,
|
&self,
|
||||||
handle: &Model<T>,
|
handle: &Model<T>,
|
||||||
|
@ -71,6 +63,14 @@ impl Context for TestAppContext {
|
||||||
app.read_model(handle, read)
|
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>(
|
fn read_window<T, R>(
|
||||||
&self,
|
&self,
|
||||||
window: &WindowHandle<T>,
|
window: &WindowHandle<T>,
|
||||||
|
@ -309,7 +309,7 @@ impl TestAppContext {
|
||||||
/// sets the global in this context.
|
/// sets the global in this context.
|
||||||
pub fn set_global<G: Global>(&mut self, global: G) {
|
pub fn set_global<G: Global>(&mut self, global: G) {
|
||||||
let mut lock = self.app.borrow_mut();
|
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)
|
/// 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,
|
update: impl FnOnce(&mut G, &mut AppContext) -> R,
|
||||||
) -> R {
|
) -> R {
|
||||||
let mut lock = self.app.borrow_mut();
|
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
|
/// 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 {
|
pub trait BorrowAppContext {
|
||||||
/// Set a global value on the context.
|
/// Set a global value on the context.
|
||||||
fn set_global<T: Global>(&mut self, global: T);
|
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
|
impl<C> BorrowAppContext for C
|
||||||
|
@ -270,6 +274,16 @@ where
|
||||||
fn set_global<G: Global>(&mut self, global: G) {
|
fn set_global<G: Global>(&mut self, global: G) {
|
||||||
self.borrow_mut().set_global(global)
|
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.
|
/// 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.
|
/// 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.
|
/// 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)))
|
.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) {
|
fn window_bounds_changed(&mut self) {
|
||||||
self.window.scale_factor = self.window.platform_window.scale_factor();
|
self.window.scale_factor = self.window.platform_window.scale_factor();
|
||||||
self.window.viewport_size = self.window.platform_window.content_size();
|
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))
|
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.
|
/// Register a callback to be invoked when the given global state changes.
|
||||||
pub fn observe_global<G: Global>(
|
pub fn observe_global<G: Global>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::Buffer;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use futures::FutureExt as _;
|
use futures::FutureExt as _;
|
||||||
use gpui::{AppContext, Model};
|
use gpui::{AppContext, BorrowAppContext, Model};
|
||||||
use gpui::{Context, TestAppContext};
|
use gpui::{Context, TestAppContext};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use proto::deserialize_operation;
|
use proto::deserialize_operation;
|
||||||
|
|
|
@ -456,7 +456,7 @@ impl LspLogView {
|
||||||
editor.set_text(log_contents, cx);
|
editor.set_text(log_contents, cx);
|
||||||
editor.move_to_end(&MoveToEnd, cx);
|
editor.move_to_end(&MoveToEnd, cx);
|
||||||
editor.set_read_only(true);
|
editor.set_read_only(true);
|
||||||
editor.set_show_copilot_suggestions(false);
|
editor.set_show_inline_completions(false);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
let editor_subscription = cx.subscribe(
|
let editor_subscription = cx.subscribe(
|
||||||
|
|
|
@ -293,7 +293,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::{Context, TestAppContext};
|
use gpui::{BorrowAppContext, Context, TestAppContext};
|
||||||
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
|
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
|
|
|
@ -180,7 +180,7 @@ async fn get_cached_server_binary(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::{Context, ModelContext, TestAppContext};
|
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
|
||||||
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
|
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
|
|
|
@ -419,7 +419,7 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::language;
|
use crate::language;
|
||||||
use gpui::{Context, Hsla, TestAppContext};
|
use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
|
||||||
use language::language_settings::AllLanguageSettings;
|
use language::language_settings::AllLanguageSettings;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use text::BufferId;
|
use text::BufferId;
|
||||||
|
|
|
@ -32,8 +32,8 @@ use futures::{
|
||||||
};
|
};
|
||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, Context, Entity, EventEmitter,
|
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
|
||||||
Model, ModelContext, PromptLevel, Task, WeakModel,
|
EventEmitter, Model, ModelContext, PromptLevel, Task, WeakModel,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{settings_store::SettingsStore, Settings};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::{channel::mpsc, StreamExt};
|
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 std::{io::ErrorKind, path::PathBuf, sync::Arc, time::Duration};
|
||||||
use util::{paths, ResultExt};
|
use util::{paths, ResultExt};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::{btree_map, hash_map, BTreeMap, HashMap};
|
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 lazy_static::lazy_static;
|
||||||
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
|
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
|
||||||
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::{insert::NormalBefore, Vim, VimModeSetting};
|
use crate::{insert::NormalBefore, Vim, VimModeSetting};
|
||||||
use editor::{Editor, EditorEvent};
|
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};
|
use settings::{Settings, SettingsStore};
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use editor::test::editor_test_context::ContextHandle;
|
use editor::test::editor_test_context::ContextHandle;
|
||||||
use gpui::{px, size, Context};
|
use gpui::{px, size, BorrowAppContext, Context};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
|
|
|
@ -37,6 +37,7 @@ use serde_derive::Serialize;
|
||||||
use settings::{update_settings_file, Settings, SettingsStore};
|
use settings::{update_settings_file, Settings, SettingsStore};
|
||||||
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
|
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
|
use ui::BorrowAppContext;
|
||||||
use visual::{visual_block_motion, visual_replace};
|
use visual::{visual_block_motion, visual_replace};
|
||||||
use workspace::{self, Workspace};
|
use workspace::{self, Workspace};
|
||||||
|
|
||||||
|
|
|
@ -4901,8 +4901,8 @@ mod tests {
|
||||||
};
|
};
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
|
px, BorrowAppContext, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView,
|
||||||
VisualTestContext,
|
Render, TestAppContext, VisualTestContext,
|
||||||
};
|
};
|
||||||
use project::{Project, ProjectEntryId};
|
use project::{Project, ProjectEntryId};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
|
@ -7,7 +7,7 @@ use client::Client;
|
||||||
use clock::FakeSystemClock;
|
use clock::FakeSystemClock;
|
||||||
use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
|
use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
|
||||||
use git::GITIGNORE;
|
use git::GITIGNORE;
|
||||||
use gpui::{ModelContext, Task, TestAppContext};
|
use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::stream::Stream;
|
use postage::stream::Stream;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
|
@ -8,14 +8,18 @@ use backtrace::Backtrace;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use clap::{command, Parser};
|
use clap::{command, Parser};
|
||||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
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 collab_ui::channel_view::ChannelView;
|
||||||
|
use copilot::Copilot;
|
||||||
|
use copilot_ui::CopilotCompletionProvider;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::Editor;
|
use editor::{Editor, EditorMode};
|
||||||
use env_logger::Builder;
|
use env_logger::Builder;
|
||||||
use fs::RealFs;
|
use fs::RealFs;
|
||||||
use futures::{future, StreamExt};
|
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 image_viewer;
|
||||||
use isahc::{prelude::Configurable, Request};
|
use isahc::{prelude::Configurable, Request};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
|
@ -176,6 +180,7 @@ fn main() {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
assistant::init(client.clone(), cx);
|
assistant::init(client.clone(), cx);
|
||||||
|
init_inline_completion_provider(client.telemetry().clone(), cx);
|
||||||
|
|
||||||
extension::init(
|
extension::init(
|
||||||
fs.clone(),
|
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) {
|
fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gpui::BorrowAppContext;
|
||||||
|
|
||||||
let path = {
|
let path = {
|
||||||
let p = Path::new("assets/icons/file_icons/file_types.json");
|
let p = Path::new("assets/icons/file_icons/file_types.json");
|
||||||
let Ok(full_path) = p.canonicalize() else {
|
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))]
|
#[cfg(not(debug_assertions))]
|
||||||
fn watch_file_types(_fs: Arc<dyn fs::Fs>, _cx: &mut AppContext) {}
|
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 collections::HashSet;
|
||||||
use editor::{scroll::Autoscroll, DisplayPoint, Editor};
|
use editor::{scroll::Autoscroll, DisplayPoint, Editor};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext,
|
actions, Action, AnyWindowHandle, AppContext, AssetSource, BorrowAppContext, Entity,
|
||||||
VisualTestContext, WindowHandle,
|
TestAppContext, VisualTestContext, WindowHandle,
|
||||||
};
|
};
|
||||||
use language::{LanguageMatcher, LanguageRegistry};
|
use language::{LanguageMatcher, LanguageRegistry};
|
||||||
use project::{Project, ProjectPath, WorktreeSettings};
|
use project::{Project, ProjectPath, WorktreeSettings};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue