mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-29 14:43:55 +03:00
Merge pull request #2355 from zed-industries/refine-copilot
Iterate some more on Copilot
This commit is contained in:
commit
781d065d0b
@ -8,6 +8,17 @@ publish = false
|
|||||||
path = "src/copilot.rs"
|
path = "src/copilot.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = [
|
||||||
|
"client/test-support",
|
||||||
|
"collections/test-support",
|
||||||
|
"gpui/test-support",
|
||||||
|
"language/test-support",
|
||||||
|
"lsp/test-support",
|
||||||
|
"settings/test-support",
|
||||||
|
"util/test-support",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
@ -30,6 +41,7 @@ smol = "1.2.5"
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mod request;
|
pub mod request;
|
||||||
mod sign_in;
|
mod sign_in;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
@ -125,12 +125,8 @@ enum CopilotServer {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum SignInStatus {
|
enum SignInStatus {
|
||||||
Authorized {
|
Authorized,
|
||||||
_user: String,
|
Unauthorized,
|
||||||
},
|
|
||||||
Unauthorized {
|
|
||||||
_user: String,
|
|
||||||
},
|
|
||||||
SigningIn {
|
SigningIn {
|
||||||
prompt: Option<request::PromptUserDeviceFlow>,
|
prompt: Option<request::PromptUserDeviceFlow>,
|
||||||
task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
|
task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
|
||||||
@ -238,6 +234,23 @@ impl Copilot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) {
|
||||||
|
let (server, fake_server) =
|
||||||
|
LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
|
||||||
|
let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
|
||||||
|
let this = cx.add_model(|cx| Self {
|
||||||
|
http: http.clone(),
|
||||||
|
node_runtime: NodeRuntime::new(http, cx.background().clone()),
|
||||||
|
server: CopilotServer::Started {
|
||||||
|
server: Arc::new(server),
|
||||||
|
status: SignInStatus::Authorized,
|
||||||
|
subscriptions_by_buffer_id: Default::default(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(this, fake_server)
|
||||||
|
}
|
||||||
|
|
||||||
fn start_language_server(
|
fn start_language_server(
|
||||||
http: Arc<dyn HttpClient>,
|
http: Arc<dyn HttpClient>,
|
||||||
node_runtime: Arc<NodeRuntime>,
|
node_runtime: Arc<NodeRuntime>,
|
||||||
@ -617,14 +630,10 @@ impl Copilot {
|
|||||||
) {
|
) {
|
||||||
if let CopilotServer::Started { status, .. } = &mut self.server {
|
if let CopilotServer::Started { status, .. } = &mut self.server {
|
||||||
*status = match lsp_status {
|
*status = match lsp_status {
|
||||||
request::SignInStatus::Ok { user }
|
request::SignInStatus::Ok { .. }
|
||||||
| request::SignInStatus::MaybeOk { user }
|
| request::SignInStatus::MaybeOk { .. }
|
||||||
| request::SignInStatus::AlreadySignedIn { user } => {
|
| request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized,
|
||||||
SignInStatus::Authorized { _user: user }
|
request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized,
|
||||||
}
|
|
||||||
request::SignInStatus::NotAuthorized { user } => {
|
|
||||||
SignInStatus::Unauthorized { _user: user }
|
|
||||||
}
|
|
||||||
request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
|
request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
|
||||||
};
|
};
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -117,7 +117,7 @@ pub struct GetCompletionsResult {
|
|||||||
pub completions: Vec<Completion>,
|
pub completions: Vec<Completion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Completion {
|
pub struct Completion {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
@ -11,6 +11,7 @@ doctest = false
|
|||||||
[features]
|
[features]
|
||||||
test-support = [
|
test-support = [
|
||||||
"rand",
|
"rand",
|
||||||
|
"copilot/test-support",
|
||||||
"text/test-support",
|
"text/test-support",
|
||||||
"language/test-support",
|
"language/test-support",
|
||||||
"gpui/test-support",
|
"gpui/test-support",
|
||||||
@ -65,6 +66,7 @@ tree-sitter-javascript = { version = "*", optional = true }
|
|||||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true }
|
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
copilot = { path = "../copilot", features = ["test-support"] }
|
||||||
text = { path = "../text", features = ["test-support"] }
|
text = { path = "../text", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
|
@ -7,7 +7,7 @@ mod wrap_map;
|
|||||||
use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||||
pub use block_map::{BlockMap, BlockPoint};
|
pub use block_map::{BlockMap, BlockPoint};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use fold_map::FoldMap;
|
use fold_map::{FoldMap, FoldOffset};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
fonts::{FontId, HighlightStyle},
|
fonts::{FontId, HighlightStyle},
|
||||||
@ -238,19 +238,22 @@ impl DisplayMap {
|
|||||||
&self,
|
&self,
|
||||||
new_suggestion: Option<Suggestion<T>>,
|
new_suggestion: Option<Suggestion<T>>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) where
|
) -> Option<Suggestion<FoldOffset>>
|
||||||
|
where
|
||||||
T: ToPoint,
|
T: ToPoint,
|
||||||
{
|
{
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
let edits = self.buffer_subscription.consume().into_inner();
|
let edits = self.buffer_subscription.consume().into_inner();
|
||||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||||
let (snapshot, edits) = self.suggestion_map.replace(new_suggestion, snapshot, edits);
|
let (snapshot, edits, old_suggestion) =
|
||||||
|
self.suggestion_map.replace(new_suggestion, snapshot, edits);
|
||||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||||
let (snapshot, edits) = self
|
let (snapshot, edits) = self
|
||||||
.wrap_map
|
.wrap_map
|
||||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||||
self.block_map.read(snapshot, edits);
|
self.block_map.read(snapshot, edits);
|
||||||
|
old_suggestion
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
|
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
|
||||||
|
@ -79,7 +79,11 @@ impl SuggestionMap {
|
|||||||
new_suggestion: Option<Suggestion<T>>,
|
new_suggestion: Option<Suggestion<T>>,
|
||||||
fold_snapshot: FoldSnapshot,
|
fold_snapshot: FoldSnapshot,
|
||||||
fold_edits: Vec<FoldEdit>,
|
fold_edits: Vec<FoldEdit>,
|
||||||
) -> (SuggestionSnapshot, Vec<SuggestionEdit>)
|
) -> (
|
||||||
|
SuggestionSnapshot,
|
||||||
|
Vec<SuggestionEdit>,
|
||||||
|
Option<Suggestion<FoldOffset>>,
|
||||||
|
)
|
||||||
where
|
where
|
||||||
T: ToPoint,
|
T: ToPoint,
|
||||||
{
|
{
|
||||||
@ -99,7 +103,8 @@ impl SuggestionMap {
|
|||||||
let mut snapshot = self.0.lock();
|
let mut snapshot = self.0.lock();
|
||||||
|
|
||||||
let mut patch = Patch::new(edits);
|
let mut patch = Patch::new(edits);
|
||||||
if let Some(suggestion) = snapshot.suggestion.take() {
|
let old_suggestion = snapshot.suggestion.take();
|
||||||
|
if let Some(suggestion) = &old_suggestion {
|
||||||
patch = patch.compose([SuggestionEdit {
|
patch = patch.compose([SuggestionEdit {
|
||||||
old: SuggestionOffset(suggestion.position.0)
|
old: SuggestionOffset(suggestion.position.0)
|
||||||
..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
|
..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
|
||||||
@ -119,7 +124,7 @@ impl SuggestionMap {
|
|||||||
|
|
||||||
snapshot.suggestion = new_suggestion;
|
snapshot.suggestion = new_suggestion;
|
||||||
snapshot.version += 1;
|
snapshot.version += 1;
|
||||||
(snapshot.clone(), patch.into_inner())
|
(snapshot.clone(), patch.into_inner(), old_suggestion)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync(
|
pub fn sync(
|
||||||
@ -589,7 +594,7 @@ mod tests {
|
|||||||
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
|
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
|
||||||
assert_eq!(suggestion_snapshot.text(), "abcdefghi");
|
assert_eq!(suggestion_snapshot.text(), "abcdefghi");
|
||||||
|
|
||||||
let (suggestion_snapshot, _) = suggestion_map.replace(
|
let (suggestion_snapshot, _, _) = suggestion_map.replace(
|
||||||
Some(Suggestion {
|
Some(Suggestion {
|
||||||
position: 3,
|
position: 3,
|
||||||
text: "123\n456".into(),
|
text: "123\n456".into(),
|
||||||
@ -854,7 +859,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
log::info!("replacing suggestion with {:?}", new_suggestion);
|
log::info!("replacing suggestion with {:?}", new_suggestion);
|
||||||
self.replace(new_suggestion, fold_snapshot, Default::default())
|
let (snapshot, edits, _) =
|
||||||
|
self.replace(new_suggestion, fold_snapshot, Default::default());
|
||||||
|
(snapshot, edits)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ pub use language::{char_kind, CharKind};
|
|||||||
use language::{
|
use language::{
|
||||||
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
|
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
|
||||||
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
|
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
|
||||||
Point, Selection, SelectionGoal, TransactionId,
|
Point, Rope, Selection, SelectionGoal, TransactionId,
|
||||||
};
|
};
|
||||||
use link_go_to_definition::{
|
use link_go_to_definition::{
|
||||||
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
|
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
|
||||||
@ -95,6 +95,7 @@ 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 const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
@ -1833,7 +1834,7 @@ impl Editor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.hide_copilot_suggestion(cx) {
|
if self.hide_copilot_suggestion(cx).is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2488,6 +2489,8 @@ impl Editor {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.refresh_copilot_suggestions(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = self.project.clone()?;
|
let project = self.project.clone()?;
|
||||||
@ -2799,7 +2802,7 @@ impl Editor {
|
|||||||
let (buffer, buffer_position) =
|
let (buffer, buffer_position) =
|
||||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||||
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
|
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
|
||||||
cx.background().timer(Duration::from_millis(75)).await;
|
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
|
||||||
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
|
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
|
||||||
(
|
(
|
||||||
copilot.completions(&buffer, buffer_position, cx),
|
copilot.completions(&buffer, buffer_position, cx),
|
||||||
@ -2830,14 +2833,13 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
||||||
if self.copilot_state.completions.is_empty() {
|
if !self.has_active_copilot_suggestion(cx) {
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.copilot_state.active_completion_index =
|
self.copilot_state.active_completion_index =
|
||||||
(self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
|
(self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
|
||||||
|
|
||||||
self.update_visible_copilot_suggestion(cx);
|
self.update_visible_copilot_suggestion(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2846,7 +2848,7 @@ impl Editor {
|
|||||||
_: &copilot::PreviousSuggestion,
|
_: &copilot::PreviousSuggestion,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if self.copilot_state.completions.is_empty() {
|
if !self.has_active_copilot_suggestion(cx) {
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2857,19 +2859,12 @@ impl Editor {
|
|||||||
} else {
|
} else {
|
||||||
self.copilot_state.active_completion_index - 1
|
self.copilot_state.active_completion_index - 1
|
||||||
};
|
};
|
||||||
|
|
||||||
self.update_visible_copilot_suggestion(cx);
|
self.update_visible_copilot_suggestion(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
if let Some(text) = self.hide_copilot_suggestion(cx) {
|
||||||
let cursor = self.selections.newest_anchor().head();
|
|
||||||
if let Some(text) = self
|
|
||||||
.copilot_state
|
|
||||||
.text_for_active_completion(cursor, &snapshot)
|
|
||||||
{
|
|
||||||
self.insert_with_autoindent_mode(&text.to_string(), None, cx);
|
self.insert_with_autoindent_mode(&text.to_string(), None, cx);
|
||||||
self.hide_copilot_suggestion(cx);
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@ -2880,14 +2875,15 @@ impl Editor {
|
|||||||
self.display_map.read(cx).has_suggestion()
|
self.display_map.read(cx).has_suggestion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
|
||||||
if self.has_active_copilot_suggestion(cx) {
|
if self.has_active_copilot_suggestion(cx) {
|
||||||
self.display_map
|
let old_suggestion = self
|
||||||
|
.display_map
|
||||||
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
|
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
true
|
old_suggestion.map(|suggestion| suggestion.text)
|
||||||
} else {
|
} else {
|
||||||
false
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3234,10 +3230,6 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||||
if self.accept_copilot_suggestion(cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.move_to_next_snippet_tabstop(cx) {
|
if self.move_to_next_snippet_tabstop(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3267,8 +3259,8 @@ impl Editor {
|
|||||||
// If the selection is empty and the cursor is in the leading whitespace before the
|
// If the selection is empty and the cursor is in the leading whitespace before the
|
||||||
// suggested indentation, then auto-indent the line.
|
// suggested indentation, then auto-indent the line.
|
||||||
let cursor = selection.head();
|
let cursor = selection.head();
|
||||||
|
let current_indent = snapshot.indent_size_for_line(cursor.row);
|
||||||
if let Some(suggested_indent) = suggested_indents.get(&cursor.row).copied() {
|
if let Some(suggested_indent) = suggested_indents.get(&cursor.row).copied() {
|
||||||
let current_indent = snapshot.indent_size_for_line(cursor.row);
|
|
||||||
if cursor.column < suggested_indent.len
|
if cursor.column < suggested_indent.len
|
||||||
&& cursor.column <= current_indent.len
|
&& cursor.column <= current_indent.len
|
||||||
&& current_indent.len <= suggested_indent.len
|
&& current_indent.len <= suggested_indent.len
|
||||||
@ -3287,6 +3279,16 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept copilot suggestion if there is only one selection and the cursor is not
|
||||||
|
// in the leading whitespace.
|
||||||
|
if self.selections.count() == 1
|
||||||
|
&& cursor.column >= current_indent.len
|
||||||
|
&& self.has_active_copilot_suggestion(cx)
|
||||||
|
{
|
||||||
|
self.accept_copilot_suggestion(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, insert a hard or soft tab.
|
// Otherwise, insert a hard or soft tab.
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
|
let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
|
||||||
@ -3310,7 +3312,8 @@ 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(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3990,6 +3993,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(cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4004,6 +4008,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(cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6411,6 +6416,9 @@ impl Editor {
|
|||||||
multi_buffer::Event::Edited => {
|
multi_buffer::Event::Edited => {
|
||||||
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) {
|
||||||
|
self.update_visible_copilot_suggestion(cx);
|
||||||
|
}
|
||||||
cx.emit(Event::BufferEdited);
|
cx.emit(Event::BufferEdited);
|
||||||
}
|
}
|
||||||
multi_buffer::Event::ExcerptsAdded {
|
multi_buffer::Event::ExcerptsAdded {
|
||||||
|
@ -16,7 +16,7 @@ use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegist
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::FakeFs;
|
use project::FakeFs;
|
||||||
use settings::EditorSettings;
|
use settings::EditorSettings;
|
||||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::{
|
use util::{
|
||||||
assert_set_eq,
|
assert_set_eq,
|
||||||
@ -4585,81 +4585,6 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
|||||||
cx.assert_editor_state("editor.closeˇ");
|
cx.assert_editor_state("editor.closeˇ");
|
||||||
handle_resolve_completion_request(&mut cx, None).await;
|
handle_resolve_completion_request(&mut cx, None).await;
|
||||||
apply_additional_edits.await.unwrap();
|
apply_additional_edits.await.unwrap();
|
||||||
|
|
||||||
// Handle completion request passing a marked string specifying where the completion
|
|
||||||
// should be triggered from using '|' character, what range should be replaced, and what completions
|
|
||||||
// should be returned using '<' and '>' to delimit the range
|
|
||||||
async fn handle_completion_request<'a>(
|
|
||||||
cx: &mut EditorLspTestContext<'a>,
|
|
||||||
marked_string: &str,
|
|
||||||
completions: Vec<&'static str>,
|
|
||||||
) {
|
|
||||||
let complete_from_marker: TextRangeMarker = '|'.into();
|
|
||||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
|
||||||
let (_, mut marked_ranges) = marked_text_ranges_by(
|
|
||||||
marked_string,
|
|
||||||
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
|
||||||
);
|
|
||||||
|
|
||||||
let complete_from_position =
|
|
||||||
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
|
|
||||||
let replace_range =
|
|
||||||
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
|
||||||
|
|
||||||
cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
|
|
||||||
let completions = completions.clone();
|
|
||||||
async move {
|
|
||||||
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
|
||||||
assert_eq!(
|
|
||||||
params.text_document_position.position,
|
|
||||||
complete_from_position
|
|
||||||
);
|
|
||||||
Ok(Some(lsp::CompletionResponse::Array(
|
|
||||||
completions
|
|
||||||
.iter()
|
|
||||||
.map(|completion_text| lsp::CompletionItem {
|
|
||||||
label: completion_text.to_string(),
|
|
||||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
|
||||||
range: replace_range,
|
|
||||||
new_text: completion_text.to_string(),
|
|
||||||
})),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.next()
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_resolve_completion_request<'a>(
|
|
||||||
cx: &mut EditorLspTestContext<'a>,
|
|
||||||
edits: Option<Vec<(&'static str, &'static str)>>,
|
|
||||||
) {
|
|
||||||
let edits = edits.map(|edits| {
|
|
||||||
edits
|
|
||||||
.iter()
|
|
||||||
.map(|(marked_string, new_text)| {
|
|
||||||
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
|
||||||
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
|
||||||
lsp::TextEdit::new(replace_range, new_text.to_string())
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
|
||||||
let edits = edits.clone();
|
|
||||||
async move {
|
|
||||||
Ok(lsp::CompletionItem {
|
|
||||||
additional_text_edits: edits,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.next()
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@ -5956,6 +5881,223 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||||
|
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||||
|
cx.update(|cx| cx.set_global(copilot));
|
||||||
|
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ˇ
|
||||||
|
two
|
||||||
|
three
|
||||||
|
"});
|
||||||
|
|
||||||
|
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||||
|
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: "copilot1".into(),
|
||||||
|
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
deterministic.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");
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
oneˇ
|
||||||
|
two
|
||||||
|
three
|
||||||
|
"});
|
||||||
|
|
||||||
|
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||||
|
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![],
|
||||||
|
);
|
||||||
|
deterministic.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");
|
||||||
|
deterministic.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![],
|
||||||
|
);
|
||||||
|
deterministic.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));
|
||||||
|
deterministic.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}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||||
let point = DisplayPoint::new(row as u32, column as u32);
|
let point = DisplayPoint::new(row as u32, column as u32);
|
||||||
point..point
|
point..point
|
||||||
@ -5971,3 +6113,106 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
|
|||||||
marked_text
|
marked_text
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle completion request passing a marked string specifying where the completion
|
||||||
|
/// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||||
|
/// should be returned using '<' and '>' to delimit the range
|
||||||
|
fn handle_completion_request<'a>(
|
||||||
|
cx: &mut EditorLspTestContext<'a>,
|
||||||
|
marked_string: &str,
|
||||||
|
completions: Vec<&'static str>,
|
||||||
|
) -> impl Future<Output = ()> {
|
||||||
|
let complete_from_marker: TextRangeMarker = '|'.into();
|
||||||
|
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||||
|
let (_, mut marked_ranges) = marked_text_ranges_by(
|
||||||
|
marked_string,
|
||||||
|
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let complete_from_position =
|
||||||
|
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
|
||||||
|
let replace_range =
|
||||||
|
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||||
|
|
||||||
|
let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
|
||||||
|
let completions = completions.clone();
|
||||||
|
async move {
|
||||||
|
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
||||||
|
assert_eq!(
|
||||||
|
params.text_document_position.position,
|
||||||
|
complete_from_position
|
||||||
|
);
|
||||||
|
Ok(Some(lsp::CompletionResponse::Array(
|
||||||
|
completions
|
||||||
|
.iter()
|
||||||
|
.map(|completion_text| lsp::CompletionItem {
|
||||||
|
label: completion_text.to_string(),
|
||||||
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||||
|
range: replace_range,
|
||||||
|
new_text: completion_text.to_string(),
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async move {
|
||||||
|
request.next().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_resolve_completion_request<'a>(
|
||||||
|
cx: &mut EditorLspTestContext<'a>,
|
||||||
|
edits: Option<Vec<(&'static str, &'static str)>>,
|
||||||
|
) -> impl Future<Output = ()> {
|
||||||
|
let edits = edits.map(|edits| {
|
||||||
|
edits
|
||||||
|
.iter()
|
||||||
|
.map(|(marked_string, new_text)| {
|
||||||
|
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
||||||
|
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
||||||
|
lsp::TextEdit::new(replace_range, new_text.to_string())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut request =
|
||||||
|
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||||
|
let edits = edits.clone();
|
||||||
|
async move {
|
||||||
|
Ok(lsp::CompletionItem {
|
||||||
|
additional_text_edits: edits,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async move {
|
||||||
|
request.next().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user