Compare commits

...
Sign in to create a new pull request.

24 commits

Author SHA1 Message Date
Max Brunsfeld
949525e77d zed 0.82.9 2023-04-24 14:58:31 -07:00
Max Brunsfeld
7ee665bbc0 Merge pull request #2409 from zed-industries/stale-excerpts
Fix stale project diagnostic excerpts for guests
2023-04-24 14:53:35 -07:00
Mikayla Maki
397c3efbde zed 0.82.8 2023-04-21 11:02:06 -07:00
Mikayla Maki
389ce38e5d Merge pull request #2402 from zed-industries/fix-panics
Fix panic in pane.rs
2023-04-21 11:01:23 -07:00
Max Brunsfeld
5799f0f10f v0.82.x stable 2023-04-20 09:51:57 -07:00
Mikayla Maki
89496c0285 Merge pull request #2395 from zed-industries/remove-stable-hiding-copilot
Remove stable guard for copilot
2023-04-20 09:43:01 -07:00
Mikayla Maki
f5407fa19a Fix underflow potential 2023-04-20 09:42:55 -07:00
Mikayla Maki
aba8893cef zed 0.82.7 2023-04-19 17:49:36 -07:00
Mikayla Maki
cdef1c1db7 Merge pull request #2386 from zed-industries/copilot-shipping
Get copilot ready to ship
2023-04-19 17:49:04 -07:00
Nate Butler
2a899aa702 Merge pull request #2384 from zed-industries/update-copilot-styles
Update copilot styles
2023-04-19 17:48:56 -07:00
Max Brunsfeld
d21238f5a0 zed 0.82.6 2023-04-18 16:45:32 -07:00
Max Brunsfeld
1722fc9fe2 Merge pull request #2387 from zed-industries/panic-in-set-selections-from-remote
Fix 'invalid insertion' panic when following
2023-04-18 16:45:02 -07:00
Antonio Scandurra
6ae29d3bb0 zed 0.82.5 2023-04-18 11:21:36 +02:00
Antonio Scandurra
3afbe51e30 Merge pull request #2383 from zed-industries/show-copilot-more-often
Clean up completion tasks, even if they fail or return no results
2023-04-18 11:21:02 +02:00
Max Brunsfeld
79df2a4f3c zed 0.82.4 2023-04-17 15:57:23 -07:00
Max Brunsfeld
a4c45236e3 Merge pull request #2381 from zed-industries/fix-buffer-latency
Send buffer operations in batches to reduce latency
2023-04-17 15:56:43 -07:00
Max Brunsfeld
6dde8a9b59 zed 0.82.3 2023-04-14 09:55:47 -07:00
Max Brunsfeld
ccde289ed7 Merge pull request #2375 from zed-industries/worktree-scan-id-fix
Always bump worktree's scan_id when refreshing an entry
2023-04-14 09:55:21 -07:00
Max Brunsfeld
9a026f3c4b zed 0.82.2 2023-04-13 12:50:49 -07:00
Max Brunsfeld
301609d595 Merge pull request #2371 from zed-industries/refresh-entry-delay
Restructure background scanner to handle refresh requests even while scanning directories
2023-04-13 12:49:52 -07:00
Joseph T. Lyons
ea8dba625d Merge pull request #2374 from zed-industries/add-vim-mode-metric
Add vim mode metric
2023-04-13 13:53:57 -04:00
Antonio Scandurra
b089be40ba zed 0.82.1 2023-04-13 10:52:39 +02:00
Antonio Scandurra
b83451ccf6 Merge pull request #2373 from zed-industries/fix-copilot-panic
Avoid interpolating Copilot suggestion if cursor excerpt differs
2023-04-13 10:50:15 +02:00
Joseph Lyons
50658077a0 v0.82.x preview 2023-04-12 13:31:19 -04:00
27 changed files with 1675 additions and 968 deletions

3
Cargo.lock generated
View file

@ -1354,7 +1354,6 @@ dependencies = [
"smol", "smol",
"theme", "theme",
"util", "util",
"workspace",
] ]
[[package]] [[package]]
@ -8516,7 +8515,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.82.0" version = "0.82.9"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"anyhow", "anyhow",

View file

@ -177,7 +177,7 @@
"focus": false "focus": false
} }
], ],
"alt-\\": "copilot::NextSuggestion", "alt-\\": "copilot::Suggest",
"alt-]": "copilot::NextSuggestion", "alt-]": "copilot::NextSuggestion",
"alt-[": "copilot::PreviousSuggestion" "alt-[": "copilot::PreviousSuggestion"
} }

View file

@ -1,6 +1,11 @@
{ {
// The name of the Zed theme to use for the UI // The name of the Zed theme to use for the UI
"theme": "One Dark", "theme": "One Dark",
// Features that can be globally enabled or disabled
"features": {
// Show Copilot icon in status bar
"copilot": true
},
// The name of a font to use for rendering text in the editor // The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono", "buffer_font_family": "Zed Mono",
// The OpenType features to enable for text in the editor. // The OpenType features to enable for text in the editor.
@ -13,11 +18,6 @@
// The factor to grow the active pane by. Defaults to 1.0 // The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes. // which gives the same size as all other panes.
"active_pane_magnification": 1.0, "active_pane_magnification": 1.0,
// Enable / disable copilot integration.
"enable_copilot_integration": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"copilot": "on",
// Whether to enable vim modes and key bindings // Whether to enable vim modes and key bindings
"vim_mode": false, "vim_mode": false,
// Whether to show the informational hover box when moving the mouse // Whether to show the informational hover box when moving the mouse
@ -30,6 +30,9 @@
// Whether to pop the completions menu while typing in an editor without // Whether to pop the completions menu while typing in an editor without
// explicitly requesting it. // explicitly requesting it.
"show_completions_on_input": true, "show_completions_on_input": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"show_copilot_suggestions": true,
// Whether the screen sharing icon is shown in the os status bar. // Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true, "show_call_status_icon": true,
// Whether to use language servers to provide code intelligence. // Whether to use language servers to provide code intelligence.

View file

@ -29,7 +29,10 @@ use std::{
env, future, mem, env, future, mem,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::Arc, sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
}; };
use unindent::Unindent as _; use unindent::Unindent as _;
use workspace::{ use workspace::{
@ -3535,6 +3538,141 @@ async fn test_collaborating_with_diagnostics(
}); });
} }
#[gpui::test(iterations = 10)]
async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
// Set up a fake language server.
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
..Default::default()
}))
.await;
client_a.language_registry.add(Arc::new(language));
let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
client_a
.fs
.insert_tree(
"/test",
json!({
"one.rs": "const ONE: usize = 1;",
"two.rs": "const TWO: usize = 2;",
"three.rs": "const THREE: usize = 3;",
"four.rs": "const FOUR: usize = 3;",
"five.rs": "const FIVE: usize = 3;",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
// Share a project as client A
let active_call_a = cx_a.read(ActiveCall::global);
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// Join the project as client B and open all three files.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
}))
.await
.unwrap();
// Simulate a language server reporting errors for a file.
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server
.request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
})
.await
.unwrap();
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
lsp::WorkDoneProgressBegin {
title: "Progress Began".into(),
..Default::default()
},
)),
});
for file_name in file_names {
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
source: Some("the-disk-based-diagnostics-source".into()),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
message: "message one".to_string(),
..Default::default()
}],
},
);
}
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
lsp::WorkDoneProgressEnd { message: None },
)),
});
// When the "disk base diagnostics finished" message is received, the buffers'
// diagnostics are expected to be present.
let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
project_b.update(cx_b, {
let project_b = project_b.clone();
let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
move |_, cx| {
cx.subscribe(&project_b, move |_, _, event, cx| {
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
disk_based_diagnostics_finished.store(true, SeqCst);
for buffer in &guest_buffers {
assert_eq!(
buffer
.read(cx)
.snapshot()
.diagnostics_in_range::<_, usize>(0..5, false)
.count(),
1,
"expected a diagnostic for buffer {:?}",
buffer.read(cx).file().unwrap().path(),
);
}
}
})
.detach();
}
});
deterministic.run_until_parked();
assert!(disk_based_diagnostics_finished.load(SeqCst));
}
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_collaborating_with_completion( async fn test_collaborating_with_completion(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
@ -5862,10 +6000,17 @@ async fn test_basic_following(
// Client A updates their selections in those editors // Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| { editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([0..1])) editor.handle_input("a", cx);
editor.handle_input("b", cx);
editor.handle_input("c", cx);
editor.select_left(&Default::default(), cx);
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
}); });
editor_a2.update(cx_a, |editor, cx| { editor_a2.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..3])) editor.handle_input("d", cx);
editor.handle_input("e", cx);
editor.select_left(&Default::default(), cx);
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
}); });
// When client B starts following client A, all visible view states are replicated to client B. // When client B starts following client A, all visible view states are replicated to client B.
@ -5878,6 +6023,27 @@ async fn test_basic_following(
.await .await
.unwrap(); .unwrap();
cx_c.foreground().run_until_parked();
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
});
assert_eq!(
cx_b.read(|cx| editor_b2.project_path(cx)),
Some((worktree_id, "2.txt").into())
);
assert_eq!(
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![2..1]
);
assert_eq!(
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![3..2]
);
cx_c.foreground().run_until_parked(); cx_c.foreground().run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global); let active_call_c = cx_c.read(ActiveCall::global);
let project_c = client_c.build_remote_project(project_id, cx_c).await; let project_c = client_c.build_remote_project(project_id, cx_c).await;
@ -6033,26 +6199,6 @@ async fn test_basic_following(
}); });
} }
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
});
assert_eq!(
cx_b.read(|cx| editor_b2.project_path(cx)),
Some((worktree_id, "2.txt").into())
);
assert_eq!(
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![2..3]
);
assert_eq!(
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![0..1]
);
// When client A activates a different editor, client B does so as well. // When client A activates a different editor, client B does so as well.
workspace_a.update(cx_a, |workspace, cx| { workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a1, cx) workspace.activate_item(&editor_a1, cx)

View file

@ -44,4 +44,3 @@ language = { path = "../language", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -21,26 +21,19 @@ use std::{
sync::Arc, sync::Arc,
}; };
use util::{ use util::{
channel::ReleaseChannel, fs::remove_matching, github::latest_github_release, http::HttpClient, fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
paths, ResultExt,
}; };
const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
actions!(copilot_auth, [SignIn, SignOut]); actions!(copilot_auth, [SignIn, SignOut]);
const COPILOT_NAMESPACE: &'static str = "copilot"; const COPILOT_NAMESPACE: &'static str = "copilot";
actions!(copilot, [NextSuggestion, PreviousSuggestion, Reinstall]); actions!(
copilot,
[Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
);
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) { pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
// Disable Copilot for stable releases.
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Stable {
cx.update_global::<collections::CommandPaletteFilter, _, _>(|filter, _cx| {
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
});
return;
}
let copilot = cx.add_model({ let copilot = cx.add_model({
let node_runtime = node_runtime.clone(); let node_runtime = node_runtime.clone();
move |cx| Copilot::start(http, node_runtime, cx) move |cx| Copilot::start(http, node_runtime, cx)
@ -172,7 +165,7 @@ impl Copilot {
let http = http.clone(); let http = http.clone();
let node_runtime = node_runtime.clone(); let node_runtime = node_runtime.clone();
move |this, cx| { move |this, cx| {
if cx.global::<Settings>().enable_copilot_integration { if cx.global::<Settings>().features.copilot {
if matches!(this.server, CopilotServer::Disabled) { if matches!(this.server, CopilotServer::Disabled) {
let start_task = cx let start_task = cx
.spawn({ .spawn({
@ -194,12 +187,14 @@ impl Copilot {
}) })
.detach(); .detach();
if cx.global::<Settings>().enable_copilot_integration { if cx.global::<Settings>().features.copilot {
let start_task = cx let start_task = cx
.spawn({ .spawn({
let http = http.clone(); let http = http.clone();
let node_runtime = node_runtime.clone(); let node_runtime = node_runtime.clone();
move |this, cx| Self::start_language_server(http, node_runtime, this, cx) move |this, cx| async {
Self::start_language_server(http, node_runtime, this, cx).await
}
}) })
.shared(); .shared();
@ -254,13 +249,6 @@ impl Copilot {
cx.clone(), cx.clone(),
)?; )?;
let server = server.initialize(Default::default()).await?;
let status = server
.request::<request::CheckStatus>(request::CheckStatusParams {
local_checks_only: false,
})
.await?;
server server
.on_notification::<LogMessage, _>(|params, _cx| { .on_notification::<LogMessage, _>(|params, _cx| {
match params.level { match params.level {
@ -281,6 +269,13 @@ impl Copilot {
) )
.detach(); .detach();
let server = server.initialize(Default::default()).await?;
let status = server
.request::<request::CheckStatus>(request::CheckStatusParams {
local_checks_only: false,
})
.await?;
anyhow::Ok((server, status)) anyhow::Ok((server, status))
}; };

View file

@ -146,8 +146,8 @@ pub enum LogMessage {}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LogMessageParams { pub struct LogMessageParams {
pub message: String,
pub level: u8, pub level: u8,
pub message: String,
pub metadata_str: String, pub metadata_str: String,
pub extra: Vec<String>, pub extra: Vec<String>,
} }

View file

@ -2,12 +2,18 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{ use gpui::{
elements::*, elements::*,
geometry::rect::RectF, geometry::rect::RectF,
impl_internal_actions,
platform::{WindowBounds, WindowKind, WindowOptions}, platform::{WindowBounds, WindowKind, WindowOptions},
AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle,
}; };
use settings::Settings; use settings::Settings;
use theme::ui::modal; use theme::ui::modal;
#[derive(PartialEq, Eq, Debug, Clone)]
struct ClickedConnect;
impl_internal_actions!(copilot_verification, [ClickedConnect]);
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Eq, Debug, Clone)]
struct CopyUserCode; struct CopyUserCode;
@ -17,45 +23,51 @@ struct OpenGithub;
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
let copilot = Copilot::global(cx).unwrap(); if let Some(copilot) = Copilot::global(cx) {
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
cx.observe(&copilot, move |copilot, cx| {
let status = copilot.read(cx).status();
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None; match &status {
cx.observe(&copilot, move |copilot, cx| { crate::Status::SigningIn { prompt } => {
let status = copilot.read(cx).status(); if let Some(code_verification_handle) = code_verification.as_mut() {
if cx.has_window(code_verification_handle.window_id()) {
match &status { code_verification_handle.update(cx, |code_verification_view, cx| {
crate::Status::SigningIn { prompt } => { code_verification_view.set_status(status, cx)
if let Some(code_verification_handle) = code_verification.as_mut() { });
if cx.has_window(code_verification_handle.window_id()) { cx.activate_window(code_verification_handle.window_id());
code_verification_handle.update(cx, |code_verification_view, cx| { } else {
code_verification_view.set_status(status, cx) create_copilot_auth_window(cx, &status, &mut code_verification);
}); }
cx.activate_window(code_verification_handle.window_id()); } else if let Some(_prompt) = prompt {
} else {
create_copilot_auth_window(cx, &status, &mut code_verification); create_copilot_auth_window(cx, &status, &mut code_verification);
} }
} else if let Some(_prompt) = prompt {
create_copilot_auth_window(cx, &status, &mut code_verification);
} }
} Status::Authorized | Status::Unauthorized => {
Status::Authorized | Status::Unauthorized => { if let Some(code_verification) = code_verification.as_ref() {
if let Some(code_verification) = code_verification.as_ref() { code_verification.update(cx, |code_verification, cx| {
code_verification.update(cx, |code_verification, cx| { code_verification.set_status(status, cx)
code_verification.set_status(status, cx) });
});
cx.platform().activate(true); cx.platform().activate(true);
cx.activate_window(code_verification.window_id()); cx.activate_window(code_verification.window_id());
}
}
_ => {
if let Some(code_verification) = code_verification.take() {
cx.remove_window(code_verification.window_id());
}
} }
} }
_ => { })
if let Some(code_verification) = code_verification.take() { .detach();
cx.remove_window(code_verification.window_id());
} cx.add_action(
} |code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
} code_verification.connect_clicked = true;
}) },
.detach(); );
}
} }
fn create_copilot_auth_window( fn create_copilot_auth_window(
@ -81,11 +93,15 @@ fn create_copilot_auth_window(
pub struct CopilotCodeVerification { pub struct CopilotCodeVerification {
status: Status, status: Status,
connect_clicked: bool,
} }
impl CopilotCodeVerification { impl CopilotCodeVerification {
pub fn new(status: Status) -> Self { pub fn new(status: Status) -> Self {
Self { status } Self {
status,
connect_clicked: false,
}
} }
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) { pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
@ -143,6 +159,7 @@ impl CopilotCodeVerification {
} }
fn render_prompting_modal( fn render_prompting_modal(
connect_clicked: bool,
data: &PromptUserDeviceFlow, data: &PromptUserDeviceFlow,
style: &theme::Copilot, style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>, cx: &mut gpui::RenderContext<Self>,
@ -189,13 +206,20 @@ impl CopilotCodeVerification {
.with_style(style.auth.prompting.hint.container.clone()) .with_style(style.auth.prompting.hint.container.clone())
.boxed(), .boxed(),
theme::ui::cta_button_with_click( theme::ui::cta_button_with_click(
"Connect to GitHub", if connect_clicked {
"Waiting for connection..."
} else {
"Connect to GitHub"
},
style.auth.content_width, style.auth.content_width,
&style.auth.cta_button, &style.auth.cta_button,
cx, cx,
{ {
let verification_uri = data.verification_uri.clone(); let verification_uri = data.verification_uri.clone();
move |_, cx| cx.platform().open_url(&verification_uri) move |_, cx| {
cx.platform().open_url(&verification_uri);
cx.dispatch_action(ClickedConnect)
}
}, },
) )
.boxed(), .boxed(),
@ -343,9 +367,20 @@ impl View for CopilotCodeVerification {
match &self.status { match &self.status {
Status::SigningIn { Status::SigningIn {
prompt: Some(prompt), prompt: Some(prompt),
} => Self::render_prompting_modal(&prompt, &style.copilot, cx), } => Self::render_prompting_modal(
Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx), self.connect_clicked,
Status::Authorized => Self::render_enabled_modal(&style.copilot, cx), &prompt,
&style.copilot,
cx,
),
Status::Unauthorized => {
self.connect_clicked = false;
Self::render_unauthorized_modal(&style.copilot, cx)
}
Status::Authorized => {
self.connect_clicked = false;
Self::render_enabled_modal(&style.copilot, cx)
}
_ => Empty::new().boxed(), _ => Empty::new().boxed(),
}, },
]) ])

View file

@ -24,6 +24,15 @@ const COPILOT_ERROR_TOAST_ID: usize = 1338;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct DeployCopilotMenu; pub struct DeployCopilotMenu;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotStartMenu;
#[derive(Clone, PartialEq)]
pub struct HideCopilot;
#[derive(Clone, PartialEq)]
pub struct InitiateSignIn;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct ToggleCopilotForLanguage { pub struct ToggleCopilotForLanguage {
language: Arc<str>, language: Arc<str>,
@ -40,6 +49,9 @@ impl_internal_actions!(
copilot, copilot,
[ [
DeployCopilotMenu, DeployCopilotMenu,
DeployCopilotStartMenu,
HideCopilot,
InitiateSignIn,
DeployCopilotModal, DeployCopilotModal,
ToggleCopilotForLanguage, ToggleCopilotForLanguage,
ToggleCopilotGlobally, ToggleCopilotGlobally,
@ -48,17 +60,19 @@ impl_internal_actions!(
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(CopilotButton::deploy_copilot_menu); cx.add_action(CopilotButton::deploy_copilot_menu);
cx.add_action(CopilotButton::deploy_copilot_start_menu);
cx.add_action( cx.add_action(
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| { |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
let language = action.language.to_owned(); let language = action.language.clone();
let show_copilot_suggestions = cx
let current_langauge = cx.global::<Settings>().copilot_on(Some(&language)); .global::<Settings>()
.show_copilot_suggestions(Some(&language));
SettingsFile::update(cx, move |file_contents| { SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert( file_contents.languages.insert(
language.to_owned(), language,
settings::EditorSettings { settings::EditorSettings {
copilot: Some((!current_langauge).into()), show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
..Default::default() ..Default::default()
}, },
); );
@ -67,12 +81,63 @@ pub fn init(cx: &mut AppContext) {
); );
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| { cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
let copilot_on = cx.global::<Settings>().copilot_on(None); let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
SettingsFile::update(cx, move |file_contents| { SettingsFile::update(cx, move |file_contents| {
file_contents.editor.copilot = Some((!copilot_on).into()) file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
}) })
}); });
cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| {
SettingsFile::update(cx, move |file_contents| {
file_contents.features.copilot = Some(false)
})
});
cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
match status {
Status::Starting { task } => {
cx.dispatch_action(workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot is starting...",
));
let window_id = cx.window_id();
let task = task.to_owned();
cx.spawn(|handle, mut cx| async move {
task.await;
cx.update(|cx| {
if let Some(copilot) = Copilot::global(cx) {
let status = copilot.read(cx).status();
match status {
Status::Authorized => cx.dispatch_action_at(
window_id,
handle.id(),
workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot has started!",
),
),
_ => {
cx.dispatch_action_at(
window_id,
handle.id(),
DismissToast::new(COPILOT_STARTING_TOAST_ID),
);
cx.dispatch_action_at(window_id, handle.id(), SignIn)
}
}
}
})
})
.detach();
}
_ => cx.dispatch_action(SignIn),
}
})
} }
pub struct CopilotButton { pub struct CopilotButton {
@ -94,7 +159,7 @@ impl View for CopilotButton {
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
let settings = cx.global::<Settings>(); let settings = cx.global::<Settings>();
if !settings.enable_copilot_integration { if !settings.features.copilot {
return Empty::new().boxed(); return Empty::new().boxed();
} }
@ -105,9 +170,9 @@ impl View for CopilotButton {
}; };
let status = copilot.read(cx).status(); let status = copilot.read(cx).status();
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); let enabled = self
.editor_enabled
let view_id = cx.view_id(); .unwrap_or(settings.show_copilot_suggestions(None));
Stack::new() Stack::new()
.with_child( .with_child(
@ -155,48 +220,13 @@ impl View for CopilotButton {
let status = status.clone(); let status = status.clone();
move |_, cx| match status { move |_, cx| match status {
Status::Authorized => cx.dispatch_action(DeployCopilotMenu), Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
Status::Starting { ref task } => {
cx.dispatch_action(workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot is starting...",
));
let window_id = cx.window_id();
let task = task.to_owned();
cx.spawn(|mut cx| async move {
task.await;
cx.update(|cx| {
if let Some(copilot) = Copilot::global(cx) {
let status = copilot.read(cx).status();
match status {
Status::Authorized => cx.dispatch_action_at(
window_id,
view_id,
workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot has started!",
),
),
_ => {
cx.dispatch_action_at(
window_id,
view_id,
DismissToast::new(COPILOT_STARTING_TOAST_ID),
);
cx.dispatch_global_action(SignIn)
}
}
}
})
})
.detach();
}
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action( Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
COPILOT_ERROR_TOAST_ID, COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e), format!("Copilot can't be started: {}", e),
"Reinstall Copilot", "Reinstall Copilot",
Reinstall, Reinstall,
)), )),
_ => cx.dispatch_action(SignIn), _ => cx.dispatch_action(DeployCopilotStartMenu),
} }
}) })
.with_tooltip::<Self, _>( .with_tooltip::<Self, _>(
@ -242,22 +272,38 @@ impl CopilotButton {
} }
} }
pub fn deploy_copilot_start_menu(
&mut self,
_: &DeployCopilotStartMenu,
cx: &mut ViewContext<Self>,
) {
let mut menu_options = Vec::with_capacity(2);
menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
menu_options.push(ContextMenuItem::item("Hide Copilot", HideCopilot));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
cx,
);
});
}
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) { pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
let settings = cx.global::<Settings>(); let settings = cx.global::<Settings>();
let mut menu_options = Vec::with_capacity(6); let mut menu_options = Vec::with_capacity(6);
if let Some(language) = &self.language { if let Some(language) = &self.language {
let language_enabled = settings.copilot_on(Some(language.as_ref())); let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
menu_options.push(ContextMenuItem::item( menu_options.push(ContextMenuItem::item(
format!( format!(
"{} Copilot for {}", "{} Suggestions for {}",
if language_enabled { if language_enabled { "Hide" } else { "Show" },
"Disable"
} else {
"Enable"
},
language language
), ),
ToggleCopilotForLanguage { ToggleCopilotForLanguage {
@ -266,12 +312,12 @@ impl CopilotButton {
)); ));
} }
let globally_enabled = cx.global::<Settings>().copilot_on(None); let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
menu_options.push(ContextMenuItem::item( menu_options.push(ContextMenuItem::item(
if globally_enabled { if globally_enabled {
"Disable Copilot Globally" "Hide Suggestions for All Files"
} else { } else {
"Enable Copilot Globally" "Show Suggestions for All Files"
}, },
ToggleCopilotGlobally, ToggleCopilotGlobally,
)); ));
@ -319,7 +365,7 @@ impl CopilotButton {
self.language = language_name.clone(); self.language = language_name.clone();
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref())); self.editor_enabled = Some(settings.show_copilot_suggestions(language_name.as_deref()));
cx.notify() cx.notify()
} }

View file

@ -20,7 +20,7 @@ mod editor_tests;
pub mod test; pub mod test;
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use anyhow::Result; use anyhow::{anyhow, Result};
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
@ -395,6 +395,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_async_action(Editor::find_all_references); cx.add_async_action(Editor::find_all_references);
cx.add_action(Editor::next_copilot_suggestion); cx.add_action(Editor::next_copilot_suggestion);
cx.add_action(Editor::previous_copilot_suggestion); cx.add_action(Editor::previous_copilot_suggestion);
cx.add_action(Editor::copilot_suggest);
hover_popover::init(cx); hover_popover::init(cx);
link_go_to_definition::init(cx); link_go_to_definition::init(cx);
@ -1014,6 +1015,8 @@ impl CodeActionsMenu {
pub struct CopilotState { pub struct CopilotState {
excerpt_id: Option<ExcerptId>, excerpt_id: Option<ExcerptId>,
pending_refresh: Task<Option<()>>, pending_refresh: Task<Option<()>>,
pending_cycling_refresh: Task<Option<()>>,
cycled: bool,
completions: Vec<copilot::Completion>, completions: Vec<copilot::Completion>,
active_completion_index: usize, active_completion_index: usize,
} }
@ -1022,9 +1025,11 @@ impl Default for CopilotState {
fn default() -> Self { fn default() -> Self {
Self { Self {
excerpt_id: None, excerpt_id: None,
pending_cycling_refresh: Task::ready(Some(())),
pending_refresh: Task::ready(Some(())), pending_refresh: Task::ready(Some(())),
completions: Default::default(), completions: Default::default(),
active_completion_index: 0, active_completion_index: 0,
cycled: false,
} }
} }
} }
@ -1040,7 +1045,8 @@ impl CopilotState {
let completion = self.completions.get(self.active_completion_index)?; let completion = self.completions.get(self.active_completion_index)?;
let excerpt_id = self.excerpt_id?; let excerpt_id = self.excerpt_id?;
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?; let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
if !completion.range.start.is_valid(completion_buffer) if excerpt_id != cursor.excerpt_id
|| !completion.range.start.is_valid(completion_buffer)
|| !completion.range.end.is_valid(completion_buffer) || !completion.range.end.is_valid(completion_buffer)
{ {
return None; return None;
@ -1067,6 +1073,26 @@ impl CopilotState {
} }
} }
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) { fn push_completion(&mut self, new_completion: copilot::Completion) {
for completion in &self.completions { for completion in &self.completions {
if *completion == new_completion { if *completion == new_completion {
@ -1264,7 +1290,7 @@ impl Editor {
cx.subscribe(&buffer, Self::on_buffer_event), cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed), cx.observe(&display_map, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()), cx.observe(&blink_manager, |_, _, cx| cx.notify()),
cx.observe_global::<Settings, _>(Self::on_settings_changed), cx.observe_global::<Settings, _>(Self::settings_changed),
], ],
}; };
this.end_selection(cx); this.end_selection(cx);
@ -2025,13 +2051,13 @@ impl Editor {
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 had_active_copilot_suggestion { if had_active_copilot_suggestion {
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
if !this.has_active_copilot_suggestion(cx) { if !this.has_active_copilot_suggestion(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(cx); this.refresh_copilot_suggestions(true, cx);
} }
}); });
} }
@ -2113,7 +2139,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(cx); this.refresh_copilot_suggestions(true, cx);
}); });
} }
@ -2351,53 +2377,66 @@ impl Editor {
let id = post_inc(&mut self.next_completion_id); let id = post_inc(&mut self.next_completion_id);
let task = cx.spawn_weak(|this, mut cx| { let task = cx.spawn_weak(|this, mut cx| {
async move { async move {
let completions = completions.await?; let menu = if let Some(completions) = completions.await.log_err() {
if completions.is_empty() { let mut menu = CompletionsMenu {
return Ok(()); id,
} initial_position: position,
match_candidates: completions
let mut menu = CompletionsMenu { .iter()
id, .enumerate()
initial_position: position, .map(|(id, completion)| {
match_candidates: completions StringMatchCandidate::new(
.iter() id,
.enumerate() completion.label.text[completion.label.filter_range.clone()]
.map(|(id, completion)| { .into(),
StringMatchCandidate::new( )
id, })
completion.label.text[completion.label.filter_range.clone()].into(), .collect(),
) buffer,
}) completions: completions.into(),
.collect(), matches: Vec::new().into(),
buffer, selected_item: 0,
completions: completions.into(), list: Default::default(),
matches: Vec::new().into(), };
selected_item: 0, menu.filter(query.as_deref(), cx.background()).await;
list: Default::default(), if menu.matches.is_empty() {
None
} else {
Some(menu)
}
} else {
None
}; };
menu.filter(query.as_deref(), cx.background()).await; let this = this
.upgrade(&cx)
.ok_or_else(|| anyhow!("editor was dropped"))?;
this.update(&mut cx, |this, cx| {
this.completion_tasks.retain(|(task_id, _)| *task_id > id);
if let Some(this) = this.upgrade(&cx) { match this.context_menu.as_ref() {
this.update(&mut cx, |this, cx| { None => {}
match this.context_menu.as_ref() { Some(ContextMenu::Completions(prev_menu)) => {
None => {} if prev_menu.id > id {
Some(ContextMenu::Completions(prev_menu)) => { return;
if prev_menu.id > menu.id {
return;
}
} }
_ => return,
} }
_ => return,
}
this.completion_tasks.retain(|(id, _)| *id > menu.id); if this.focused && menu.is_some() {
if this.focused && !menu.matches.is_empty() { let menu = menu.unwrap();
this.show_context_menu(ContextMenu::Completions(menu), cx); this.show_context_menu(ContextMenu::Completions(menu), cx);
} else if this.hide_context_menu(cx).is_none() { } else if this.completion_tasks.is_empty() {
// If there are no more completion tasks and the last menu was
// empty, we should hide it. If it was already hidden, we should
// also show the copilot suggestion when available.
if this.hide_context_menu(cx).is_none() {
this.update_visible_copilot_suggestion(cx); this.update_visible_copilot_suggestion(cx);
} }
}); }
} });
Ok::<_, anyhow::Error>(()) Ok::<_, anyhow::Error>(())
} }
.log_err() .log_err()
@ -2498,7 +2537,7 @@ impl Editor {
}); });
} }
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
}); });
let project = self.project.clone()?; let project = self.project.clone()?;
@ -2791,10 +2830,14 @@ impl Editor {
None None
} }
fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> { fn refresh_copilot_suggestions(
&mut self,
debounce: bool,
cx: &mut ViewContext<Self>,
) -> Option<()> {
let copilot = Copilot::global(cx)?; let copilot = Copilot::global(cx)?;
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
self.hide_copilot_suggestion(cx); self.clear_copilot_suggestions(cx);
return None; return None;
} }
self.update_visible_copilot_suggestion(cx); self.update_visible_copilot_suggestion(cx);
@ -2802,28 +2845,35 @@ impl Editor {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor = self.selections.newest_anchor().head(); let cursor = self.selections.newest_anchor().head();
let language_name = snapshot.language_at(cursor).map(|language| language.name()); let language_name = snapshot.language_at(cursor).map(|language| language.name());
if !cx.global::<Settings>().copilot_on(language_name.as_deref()) { if !cx
self.hide_copilot_suggestion(cx); .global::<Settings>()
.show_copilot_suggestions(language_name.as_deref())
{
self.clear_copilot_suggestions(cx);
return None; return None;
} }
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(COPILOT_DEBOUNCE_TIMEOUT).await; if debounce {
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| { cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
( }
copilot.completions(&buffer, buffer_position, cx),
copilot.completions_cycling(&buffer, buffer_position, cx), let completions = copilot
) .update(&mut cx, |copilot, cx| {
}); copilot.completions(&buffer, buffer_position, cx)
})
.await
.log_err()
.into_iter()
.flatten()
.collect_vec();
let (completion, completions_cycling) = futures::join!(completion, completions_cycling);
let mut completions = Vec::new();
completions.extend(completion.log_err().into_iter().flatten());
completions.extend(completions_cycling.log_err().into_iter().flatten());
this.upgrade(&cx)?.update(&mut cx, |this, cx| { this.upgrade(&cx)?.update(&mut cx, |this, cx| {
if !completions.is_empty() { 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.completions.clear();
this.copilot_state.active_completion_index = 0; this.copilot_state.active_completion_index = 0;
this.copilot_state.excerpt_id = Some(cursor.excerpt_id); this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
@ -2840,34 +2890,73 @@ impl Editor {
Some(()) Some(())
} }
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) { fn cycle_suggestions(
&mut self,
direction: Direction,
cx: &mut ViewContext<Self>,
) -> Option<()> {
let copilot = Copilot::global(cx)?;
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
return None;
}
if self.copilot_state.cycled {
self.copilot_state.cycle_completions(direction);
self.update_visible_copilot_suggestion(cx);
} else {
let cursor = self.selections.newest_anchor().head();
let (buffer, buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
self.copilot_state.pending_cycling_refresh = cx.spawn_weak(|this, mut cx| async move {
let completions = copilot
.update(&mut cx, |copilot, cx| {
copilot.completions_cycling(&buffer, buffer_position, cx)
})
.await;
this.upgrade(&cx)?.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);
});
Some(())
});
}
Some(())
}
fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
if !self.has_active_copilot_suggestion(cx) { if !self.has_active_copilot_suggestion(cx) {
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(false, cx);
return; return;
} }
self.copilot_state.active_completion_index =
(self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
self.update_visible_copilot_suggestion(cx); self.update_visible_copilot_suggestion(cx);
} }
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
if self.has_active_copilot_suggestion(cx) {
self.cycle_suggestions(Direction::Next, cx);
} else {
self.refresh_copilot_suggestions(false, cx);
}
}
fn previous_copilot_suggestion( fn previous_copilot_suggestion(
&mut self, &mut self,
_: &copilot::PreviousSuggestion, _: &copilot::PreviousSuggestion,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if !self.has_active_copilot_suggestion(cx) { if self.has_active_copilot_suggestion(cx) {
self.refresh_copilot_suggestions(cx); self.cycle_suggestions(Direction::Prev, cx);
return; } else {
self.refresh_copilot_suggestions(false, cx);
} }
self.copilot_state.active_completion_index =
if self.copilot_state.active_completion_index == 0 {
self.copilot_state.completions.len() - 1
} else {
self.copilot_state.active_completion_index - 1
};
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 {
@ -2909,11 +2998,11 @@ impl Editor {
.copilot_state .copilot_state
.text_for_active_completion(cursor, &snapshot) .text_for_active_completion(cursor, &snapshot)
{ {
self.display_map.update(cx, |map, cx| { self.display_map.update(cx, move |map, cx| {
map.replace_suggestion( map.replace_suggestion(
Some(Suggestion { Some(Suggestion {
position: cursor, position: cursor,
text: text.into(), text: text.trim_end().into(),
}), }),
cx, cx,
) )
@ -2924,6 +3013,11 @@ impl Editor {
} }
} }
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
self.copilot_state = Default::default();
self.hide_copilot_suggestion(cx);
}
pub fn render_code_actions_indicator( pub fn render_code_actions_indicator(
&self, &self,
style: &EditorStyle, style: &EditorStyle,
@ -3209,7 +3303,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(cx); this.refresh_copilot_suggestions(true, cx);
}); });
} }
@ -3225,7 +3319,7 @@ impl Editor {
}) })
}); });
this.insert("", cx); this.insert("", cx);
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
}); });
} }
@ -3321,7 +3415,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(cx); this.refresh_copilot_suggestions(true, cx);
}); });
} }
@ -4001,7 +4095,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); self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited); cx.emit(Event::Edited);
} }
} }
@ -4016,7 +4110,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); self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited); cx.emit(Event::Edited);
} }
} }
@ -6477,8 +6571,8 @@ impl Editor {
cx.notify(); cx.notify();
} }
fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) { fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(true, cx);
} }
pub fn set_searchable(&mut self, searchable: bool) { pub fn set_searchable(&mut self, searchable: bool) {
@ -6619,13 +6713,15 @@ impl Editor {
.as_singleton() .as_singleton()
.and_then(|b| b.read(cx).file()), .and_then(|b| b.read(cx).file()),
) { ) {
let settings = cx.global::<Settings>();
let extension = Path::new(file.file_name(cx)) let extension = Path::new(file.file_name(cx))
.extension() .extension()
.and_then(|e| e.to_str()); .and_then(|e| e.to_str());
project.read(cx).client().report_event( project.read(cx).client().report_event(
name, name,
json!({ "File Extension": extension }), json!({ "File Extension": extension, "Vim Mode": settings.vim_mode }),
cx.global::<Settings>().telemetry(), settings.telemetry(),
); );
} }
} }

View file

@ -4322,7 +4322,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
cx.set_state( cx.set_state(
&[ &[
"one ", // "one ", //
"twoˇ", // "twoˇ", //
"three ", // "three ", //
"four", // "four", //
] ]
@ -4397,7 +4397,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
&[ &[
"one", // "one", //
"", // "", //
"twoˇ", // "twoˇ", //
"", // "", //
"three", // "three", //
"four", // "four", //
@ -4412,7 +4412,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
cx.assert_editor_state( cx.assert_editor_state(
&[ &[
"one ", // "one ", //
"twoˇ", // "twoˇ", //
"three ", // "three ", //
"four", // "four", //
] ]
@ -5897,13 +5897,12 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
) )
.await; .await;
// When inserting, ensure autocompletion is favored over Copilot suggestions.
cx.set_state(indoc! {" cx.set_state(indoc! {"
oneˇ oneˇ
two two
three three
"}); "});
// When inserting, ensure autocompletion is favored over Copilot suggestions.
cx.simulate_keystroke("."); cx.simulate_keystroke(".");
let _ = handle_completion_request( let _ = handle_completion_request(
&mut cx, &mut cx,
@ -5917,8 +5916,8 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
handle_copilot_completion_request( handle_copilot_completion_request(
&copilot_lsp, &copilot_lsp,
vec![copilot::request::Completion { vec![copilot::request::Completion {
text: "copilot1".into(), text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default() ..Default::default()
}], }],
vec![], vec![],
@ -5940,13 +5939,45 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
assert_eq!(editor.display_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! {" cx.set_state(indoc! {"
oneˇ oneˇ
two two
three 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![],
);
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.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
});
// When inserting, ensure autocompletion is favored over Copilot suggestions. // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
cx.set_state(indoc! {"
oneˇ
two
three
"});
cx.simulate_keystroke("."); cx.simulate_keystroke(".");
let _ = handle_completion_request( let _ = handle_completion_request(
&mut cx, &mut cx,
@ -6163,6 +6194,110 @@ async fn test_copilot_completion_invalidation(
}); });
} }
#[gpui::test]
async fn test_copilot_multibuffer(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let (copilot, copilot_lsp) = Copilot::fake(cx);
cx.update(|cx| {
cx.set_global(Settings::test(cx));
cx.set_global(copilot)
});
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx));
let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
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);
});
deterministic.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.
deterministic.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");
});
}
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

View file

@ -3,12 +3,12 @@ use crate::{
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{Context, Result};
use collections::HashSet; use collections::HashSet;
use futures::future::try_join_all; use futures::future::try_join_all;
use gpui::{ use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, RenderContext, elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
}; };
use language::{ use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
@ -72,11 +72,11 @@ impl FollowableItem for Editor {
let editor = pane.read_with(&cx, |pane, cx| { let editor = pane.read_with(&cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>(); let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| { editors.find(|editor| {
editor.remote_id(&client, cx) == Some(remote_id) let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|| state.singleton let singleton_buffer_matches = state.singleton
&& buffers.len() == 1 && buffers.first()
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref() == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
== Some(&buffers[0]) ids_match || singleton_buffer_matches
}) })
}); });
@ -115,46 +115,29 @@ impl FollowableItem for Editor {
multibuffer multibuffer
}); });
cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx)) cx.add_view(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
editor.remote_id = Some(remote_id);
editor
})
}) })
}); });
editor.update(&mut cx, |editor, cx| { update_editor_from_message(
editor.remote_id = Some(remote_id); editor.clone(),
let buffer = editor.buffer.read(cx).read(cx); project,
let selections = state proto::update_view::Editor {
.selections selections: state.selections,
.into_iter() pending_selection: state.pending_selection,
.map(|selection| { scroll_top_anchor: state.scroll_top_anchor,
deserialize_selection(&buffer, selection) scroll_x: state.scroll_x,
.ok_or_else(|| anyhow!("invalid selection")) scroll_y: state.scroll_y,
}) ..Default::default()
.collect::<Result<Vec<_>>>()?; },
let pending_selection = state &mut cx,
.pending_selection )
.map(|selection| deserialize_selection(&buffer, selection)) .await?;
.flatten();
let scroll_top_anchor = state
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
drop(buffer);
if !selections.is_empty() || pending_selection.is_some() {
editor.set_selections_from_remote(selections, pending_selection, cx);
}
if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: scroll_top_anchor,
offset: vec2f(state.scroll_x, state.scroll_y),
},
cx,
);
}
anyhow::Ok(())
})?;
Ok(editor) Ok(editor)
})) }))
@ -299,96 +282,9 @@ impl FollowableItem for Editor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let update_view::Variant::Editor(message) = message; let update_view::Variant::Editor(message) = message;
let multibuffer = self.buffer.read(cx);
let multibuffer = multibuffer.read(cx);
let buffer_ids = message
.inserted_excerpts
.iter()
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
.collect::<HashSet<_>>();
let mut removals = message
.deleted_excerpts
.into_iter()
.map(ExcerptId::from_proto)
.collect::<Vec<_>>();
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
let selections = message
.selections
.into_iter()
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
.and_then(|selection| deserialize_selection(&multibuffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
drop(multibuffer);
let buffers = project.update(cx, |project, cx| {
buffer_ids
.into_iter()
.map(|id| project.open_buffer_by_id(id, cx))
.collect::<Vec<_>>()
});
let project = project.clone(); let project = project.clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let _buffers = try_join_all(buffers).await?; update_editor_from_message(this, project, message, &mut cx).await
this.update(&mut cx, |this, cx| {
this.buffer.update(cx, |multibuffer, cx| {
let mut insertions = message.inserted_excerpts.into_iter().peekable();
while let Some(insertion) = insertions.next() {
let Some(excerpt) = insertion.excerpt else { continue };
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
let buffer_id = excerpt.buffer_id;
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
let adjacent_excerpts = iter::from_fn(|| {
let insertion = insertions.peek()?;
if insertion.previous_excerpt_id.is_none()
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
{
insertions.next()?.excerpt
} else {
None
}
});
multibuffer.insert_excerpts_with_ids_after(
ExcerptId::from_proto(previous_excerpt_id),
buffer,
[excerpt]
.into_iter()
.chain(adjacent_excerpts)
.filter_map(|excerpt| {
Some((
ExcerptId::from_proto(excerpt.id),
deserialize_excerpt_range(excerpt)?,
))
}),
cx,
);
}
multibuffer.remove_excerpts(removals, cx);
});
if !selections.is_empty() || pending_selection.is_some() {
this.set_selections_from_remote(selections, pending_selection, cx);
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = scroll_top_anchor {
this.set_scroll_anchor_remote(ScrollAnchor {
top_anchor: anchor,
offset: vec2f(message.scroll_x, message.scroll_y)
}, cx);
}
});
Ok(())
}) })
} }
@ -402,6 +298,128 @@ impl FollowableItem for Editor {
} }
} }
async fn update_editor_from_message(
this: ViewHandle<Editor>,
project: ModelHandle<Project>,
message: proto::update_view::Editor,
cx: &mut AsyncAppContext,
) -> Result<()> {
// Open all of the buffers of which excerpts were added to the editor.
let inserted_excerpt_buffer_ids = message
.inserted_excerpts
.iter()
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
.collect::<HashSet<_>>();
let inserted_excerpt_buffers = project.update(cx, |project, cx| {
inserted_excerpt_buffer_ids
.into_iter()
.map(|id| project.open_buffer_by_id(id, cx))
.collect::<Vec<_>>()
});
let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
// Update the editor's excerpts.
this.update(cx, |editor, cx| {
editor.buffer.update(cx, |multibuffer, cx| {
let mut removed_excerpt_ids = message
.deleted_excerpts
.into_iter()
.map(ExcerptId::from_proto)
.collect::<Vec<_>>();
removed_excerpt_ids.sort_by({
let multibuffer = multibuffer.read(cx);
move |a, b| a.cmp(&b, &multibuffer)
});
let mut insertions = message.inserted_excerpts.into_iter().peekable();
while let Some(insertion) = insertions.next() {
let Some(excerpt) = insertion.excerpt else { continue };
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
let buffer_id = excerpt.buffer_id;
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
let adjacent_excerpts = iter::from_fn(|| {
let insertion = insertions.peek()?;
if insertion.previous_excerpt_id.is_none()
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
{
insertions.next()?.excerpt
} else {
None
}
});
multibuffer.insert_excerpts_with_ids_after(
ExcerptId::from_proto(previous_excerpt_id),
buffer,
[excerpt]
.into_iter()
.chain(adjacent_excerpts)
.filter_map(|excerpt| {
Some((
ExcerptId::from_proto(excerpt.id),
deserialize_excerpt_range(excerpt)?,
))
}),
cx,
);
}
multibuffer.remove_excerpts(removed_excerpt_ids, cx);
});
});
// Deserialize the editor state.
let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
let buffer = editor.buffer.read(cx).read(cx);
let selections = message
.selections
.into_iter()
.filter_map(|selection| deserialize_selection(&buffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
.and_then(|selection| deserialize_selection(&buffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
anyhow::Ok((selections, pending_selection, scroll_top_anchor))
})?;
// Wait until the buffer has received all of the operations referenced by
// the editor's new state.
this.update(cx, |editor, cx| {
editor.buffer.update(cx, |buffer, cx| {
buffer.wait_for_anchors(
selections
.iter()
.chain(pending_selection.as_ref())
.flat_map(|selection| [selection.start, selection.end])
.chain(scroll_top_anchor),
cx,
)
})
})
.await?;
// Update the editor's state.
this.update(cx, |editor, cx| {
if !selections.is_empty() || pending_selection.is_some() {
editor.set_selections_from_remote(selections, pending_selection, cx);
editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: scroll_top_anchor,
offset: vec2f(message.scroll_x, message.scroll_y),
},
cx,
);
}
});
Ok(())
}
fn serialize_excerpt( fn serialize_excerpt(
buffer_id: u64, buffer_id: u64,
id: &ExcerptId, id: &ExcerptId,

View file

@ -1,6 +1,7 @@
mod anchor; mod anchor;
pub use anchor::{Anchor, AnchorRangeExt}; pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::{anyhow, Result};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet}; use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt}; use futures::{channel::mpsc, SinkExt};
@ -16,7 +17,9 @@ use language::{
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::{Ref, RefCell}, cell::{Ref, RefCell},
cmp, fmt, io, cmp, fmt,
future::Future,
io,
iter::{self, FromIterator}, iter::{self, FromIterator},
mem, mem,
ops::{Range, RangeBounds, Sub}, ops::{Range, RangeBounds, Sub},
@ -1238,6 +1241,39 @@ impl MultiBuffer {
cx.notify(); cx.notify();
} }
pub fn wait_for_anchors<'a>(
&self,
anchors: impl 'a + Iterator<Item = Anchor>,
cx: &mut ModelContext<Self>,
) -> impl 'static + Future<Output = Result<()>> {
let borrow = self.buffers.borrow();
let mut error = None;
let mut futures = Vec::new();
for anchor in anchors {
if let Some(buffer_id) = anchor.buffer_id {
if let Some(buffer) = borrow.get(&buffer_id) {
buffer.buffer.update(cx, |buffer, _| {
futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
});
} else {
error = Some(anyhow!(
"buffer {buffer_id} is not part of this multi-buffer"
));
break;
}
}
}
async move {
if let Some(error) = error {
Err(error)?;
}
for future in futures {
future.await?;
}
Ok(())
}
}
pub fn text_anchor_for_position<T: ToOffset>( pub fn text_anchor_for_position<T: ToOffset>(
&self, &self,
position: T, position: T,

View file

@ -523,31 +523,7 @@ impl FakeFs {
} }
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) { pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
let mut state = self.state.lock(); self.write_file_internal(path, content).unwrap()
let path = path.as_ref();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(1);
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime,
content,
}));
state
.write_path(path, move |entry| {
match entry {
btree_map::Entry::Vacant(e) => {
e.insert(file);
}
btree_map::Entry::Occupied(mut e) => {
*e.get_mut() = file;
}
}
Ok(())
})
.unwrap();
state.emit_event(&[path]);
} }
pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) { pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
@ -569,6 +545,33 @@ impl FakeFs {
state.emit_event(&[path]); state.emit_event(&[path]);
} }
fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
let mut state = self.state.lock();
let path = path.as_ref();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(1);
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime,
content,
}));
state.write_path(path, move |entry| {
match entry {
btree_map::Entry::Vacant(e) => {
e.insert(file);
}
btree_map::Entry::Occupied(mut e) => {
*e.get_mut() = file;
}
}
Ok(())
})?;
state.emit_event(&[path]);
Ok(())
}
pub async fn pause_events(&self) { pub async fn pause_events(&self) {
self.state.lock().events_paused = true; self.state.lock().events_paused = true;
} }
@ -952,7 +955,7 @@ impl Fs for FakeFs {
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
self.simulate_random_delay().await; self.simulate_random_delay().await;
let path = normalize_path(path.as_path()); let path = normalize_path(path.as_path());
self.insert_file(path, data.to_string()).await; self.write_file_internal(path, data.to_string())?;
Ok(()) Ok(())
} }
@ -961,7 +964,7 @@ impl Fs for FakeFs {
self.simulate_random_delay().await; self.simulate_random_delay().await;
let path = normalize_path(path); let path = normalize_path(path);
let content = chunks(text, line_ending).collect(); let content = chunks(text, line_ending).collect();
self.insert_file(path, content).await; self.write_file_internal(path, content)?;
Ok(()) Ok(())
} }

View file

@ -1313,10 +1313,10 @@ impl Buffer {
self.text.wait_for_edits(edit_ids) self.text.wait_for_edits(edit_ids)
} }
pub fn wait_for_anchors<'a>( pub fn wait_for_anchors(
&mut self, &mut self,
anchors: impl IntoIterator<Item = &'a Anchor>, anchors: impl IntoIterator<Item = Anchor>,
) -> impl Future<Output = Result<()>> { ) -> impl 'static + Future<Output = Result<()>> {
self.text.wait_for_anchors(anchors) self.text.wait_for_anchors(anchors)
} }

View file

@ -572,7 +572,7 @@ async fn location_links_from_proto(
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin end"))?; .ok_or_else(|| anyhow!("missing origin end"))?;
buffer buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?; .await?;
Some(Location { Some(Location {
buffer, buffer,
@ -597,7 +597,7 @@ async fn location_links_from_proto(
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?; .ok_or_else(|| anyhow!("missing target end"))?;
buffer buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?; .await?;
let target = Location { let target = Location {
buffer, buffer,
@ -868,7 +868,7 @@ impl LspCommand for GetReferences {
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?; .ok_or_else(|| anyhow!("missing target end"))?;
target_buffer target_buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?; .await?;
locations.push(Location { locations.push(Location {
buffer: target_buffer, buffer: target_buffer,
@ -1012,7 +1012,7 @@ impl LspCommand for GetDocumentHighlights {
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?; .ok_or_else(|| anyhow!("missing target end"))?;
buffer buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?; .await?;
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) { let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT, Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,

View file

@ -92,7 +92,7 @@ pub trait Item {
pub struct Project { pub struct Project {
worktrees: Vec<WorktreeHandle>, worktrees: Vec<WorktreeHandle>,
active_entry: Option<ProjectEntryId>, active_entry: Option<ProjectEntryId>,
buffer_changes_tx: mpsc::UnboundedSender<BufferMessage>, buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
language_servers: HashMap<usize, LanguageServerState>, language_servers: HashMap<usize, LanguageServerState>,
language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>, language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>,
@ -131,11 +131,16 @@ pub struct Project {
terminals: Terminals, terminals: Terminals,
} }
enum BufferMessage { /// Message ordered with respect to buffer operations
enum BufferOrderedMessage {
Operation { Operation {
buffer_id: u64, buffer_id: u64,
operation: proto::Operation, operation: proto::Operation,
}, },
LanguageServerUpdate {
language_server_id: usize,
message: proto::update_language_server::Variant,
},
Resync, Resync,
} }
@ -436,11 +441,11 @@ impl Project {
) -> ModelHandle<Self> { ) -> ModelHandle<Self> {
cx.add_model(|cx: &mut ModelContext<Self>| { cx.add_model(|cx: &mut ModelContext<Self>| {
let (tx, rx) = mpsc::unbounded(); let (tx, rx) = mpsc::unbounded();
cx.spawn_weak(|this, cx| Self::send_buffer_messages(this, rx, cx)) cx.spawn_weak(|this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
.detach(); .detach();
Self { Self {
worktrees: Default::default(), worktrees: Default::default(),
buffer_changes_tx: tx, buffer_ordered_messages_tx: tx,
collaborators: Default::default(), collaborators: Default::default(),
opened_buffers: Default::default(), opened_buffers: Default::default(),
shared_buffers: Default::default(), shared_buffers: Default::default(),
@ -504,11 +509,11 @@ impl Project {
} }
let (tx, rx) = mpsc::unbounded(); let (tx, rx) = mpsc::unbounded();
cx.spawn_weak(|this, cx| Self::send_buffer_messages(this, rx, cx)) cx.spawn_weak(|this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
.detach(); .detach();
let mut this = Self { let mut this = Self {
worktrees: Vec::new(), worktrees: Vec::new(),
buffer_changes_tx: tx, buffer_ordered_messages_tx: tx,
loading_buffers_by_path: Default::default(), loading_buffers_by_path: Default::default(),
opened_buffer: watch::channel(), opened_buffer: watch::channel(),
shared_buffers: Default::default(), shared_buffers: Default::default(),
@ -1152,8 +1157,8 @@ impl Project {
) )
}) })
.collect(); .collect();
self.buffer_changes_tx self.buffer_ordered_messages_tx
.unbounded_send(BufferMessage::Resync) .unbounded_send(BufferOrderedMessage::Resync)
.unwrap(); .unwrap();
cx.notify(); cx.notify();
Ok(()) Ok(())
@ -1731,38 +1736,64 @@ impl Project {
}); });
} }
async fn send_buffer_messages( async fn send_buffer_ordered_messages(
this: WeakModelHandle<Self>, this: WeakModelHandle<Self>,
mut rx: UnboundedReceiver<BufferMessage>, rx: UnboundedReceiver<BufferOrderedMessage>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) { ) -> Option<()> {
const MAX_BATCH_SIZE: usize = 128;
let mut operations_by_buffer_id = HashMap::default();
async fn flush_operations(
this: &ModelHandle<Project>,
operations_by_buffer_id: &mut HashMap<u64, Vec<proto::Operation>>,
needs_resync_with_host: &mut bool,
is_local: bool,
cx: &AsyncAppContext,
) {
for (buffer_id, operations) in operations_by_buffer_id.drain() {
let request = this.read_with(cx, |this, _| {
let project_id = this.remote_id()?;
Some(this.client.request(proto::UpdateBuffer {
buffer_id,
project_id,
operations,
}))
});
if let Some(request) = request {
if request.await.is_err() && !is_local {
*needs_resync_with_host = true;
break;
}
}
}
}
let mut needs_resync_with_host = false; let mut needs_resync_with_host = false;
while let Some(change) = rx.next().await { let mut changes = rx.ready_chunks(MAX_BATCH_SIZE);
if let Some(this) = this.upgrade(&mut cx) {
let is_local = this.read_with(&cx, |this, _| this.is_local()); while let Some(changes) = changes.next().await {
let this = this.upgrade(&mut cx)?;
let is_local = this.read_with(&cx, |this, _| this.is_local());
for change in changes {
match change { match change {
BufferMessage::Operation { BufferOrderedMessage::Operation {
buffer_id, buffer_id,
operation, operation,
} => { } => {
if needs_resync_with_host { if needs_resync_with_host {
continue; continue;
} }
let request = this.read_with(&cx, |this, _| {
let project_id = this.remote_id()?; operations_by_buffer_id
Some(this.client.request(proto::UpdateBuffer { .entry(buffer_id)
buffer_id, .or_insert(Vec::new())
project_id, .push(operation);
operations: vec![operation],
}))
});
if let Some(request) = request {
if request.await.is_err() && !is_local {
needs_resync_with_host = true;
}
}
} }
BufferMessage::Resync => {
BufferOrderedMessage::Resync => {
operations_by_buffer_id.clear();
if this if this
.update(&mut cx, |this, cx| this.synchronize_remote_buffers(cx)) .update(&mut cx, |this, cx| this.synchronize_remote_buffers(cx))
.await .await
@ -1771,11 +1802,46 @@ impl Project {
needs_resync_with_host = false; needs_resync_with_host = false;
} }
} }
BufferOrderedMessage::LanguageServerUpdate {
language_server_id,
message,
} => {
flush_operations(
&this,
&mut operations_by_buffer_id,
&mut needs_resync_with_host,
is_local,
&cx,
)
.await;
this.read_with(&cx, |this, _| {
if let Some(project_id) = this.remote_id() {
this.client
.send(proto::UpdateLanguageServer {
project_id,
language_server_id: language_server_id as u64,
variant: Some(message),
})
.log_err();
}
});
}
} }
} else {
break;
} }
flush_operations(
&this,
&mut operations_by_buffer_id,
&mut needs_resync_with_host,
is_local,
&cx,
)
.await;
} }
None
} }
fn on_buffer_event( fn on_buffer_event(
@ -1786,8 +1852,8 @@ impl Project {
) -> Option<()> { ) -> Option<()> {
match event { match event {
BufferEvent::Operation(operation) => { BufferEvent::Operation(operation) => {
self.buffer_changes_tx self.buffer_ordered_messages_tx
.unbounded_send(BufferMessage::Operation { .unbounded_send(BufferOrderedMessage::Operation {
buffer_id: buffer.read(cx).remote_id(), buffer_id: buffer.read(cx).remote_id(),
operation: language::proto::serialize_operation(operation), operation: language::proto::serialize_operation(operation),
}) })
@ -1878,14 +1944,19 @@ impl Project {
let task = cx.spawn_weak(|this, mut cx| async move { let task = cx.spawn_weak(|this, mut cx| async move {
cx.background().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await; cx.background().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await;
if let Some(this) = this.upgrade(&cx) { if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx | { this.update(&mut cx, |this, cx| {
this.disk_based_diagnostics_finished(language_server_id, cx); this.disk_based_diagnostics_finished(
this.broadcast_language_server_update(
language_server_id, language_server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( cx,
proto::LspDiskBasedDiagnosticsUpdated {},
),
); );
this.buffer_ordered_messages_tx
.unbounded_send(
BufferOrderedMessage::LanguageServerUpdate {
language_server_id,
message:proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(Default::default())
},
)
.ok();
}); });
} }
}); });
@ -2524,12 +2595,12 @@ impl Project {
if is_disk_based_diagnostics_progress { if is_disk_based_diagnostics_progress {
language_server_status.has_pending_diagnostic_updates = true; language_server_status.has_pending_diagnostic_updates = true;
self.disk_based_diagnostics_started(server_id, cx); self.disk_based_diagnostics_started(server_id, cx);
self.broadcast_language_server_update( self.buffer_ordered_messages_tx
server_id, .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( language_server_id: server_id,
proto::LspDiskBasedDiagnosticsUpdating {}, message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(Default::default())
), })
); .ok();
} else { } else {
self.on_lsp_work_start( self.on_lsp_work_start(
server_id, server_id,
@ -2541,14 +2612,18 @@ impl Project {
}, },
cx, cx,
); );
self.broadcast_language_server_update( self.buffer_ordered_messages_tx
server_id, .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { language_server_id: server_id,
token, message: proto::update_language_server::Variant::WorkStart(
message: report.message, proto::LspWorkStart {
percentage: report.percentage.map(|p| p as u32), token,
}), message: report.message,
); percentage: report.percentage.map(|p| p as u32),
},
),
})
.ok();
} }
} }
lsp::WorkDoneProgress::Report(report) => { lsp::WorkDoneProgress::Report(report) => {
@ -2563,16 +2638,18 @@ impl Project {
}, },
cx, cx,
); );
self.broadcast_language_server_update( self.buffer_ordered_messages_tx
server_id, .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
proto::update_language_server::Variant::WorkProgress( language_server_id: server_id,
proto::LspWorkProgress { message: proto::update_language_server::Variant::WorkProgress(
token, proto::LspWorkProgress {
message: report.message, token,
percentage: report.percentage.map(|p| p as u32), message: report.message,
}, percentage: report.percentage.map(|p| p as u32),
), },
); ),
})
.ok();
} }
} }
lsp::WorkDoneProgress::End(_) => { lsp::WorkDoneProgress::End(_) => {
@ -2581,20 +2658,25 @@ impl Project {
if is_disk_based_diagnostics_progress { if is_disk_based_diagnostics_progress {
language_server_status.has_pending_diagnostic_updates = false; language_server_status.has_pending_diagnostic_updates = false;
self.disk_based_diagnostics_finished(server_id, cx); self.disk_based_diagnostics_finished(server_id, cx);
self.broadcast_language_server_update( self.buffer_ordered_messages_tx
server_id, .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( language_server_id: server_id,
proto::LspDiskBasedDiagnosticsUpdated {}, message:
), proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
); Default::default(),
),
})
.ok();
} else { } else {
self.on_lsp_work_end(server_id, token.clone(), cx); self.on_lsp_work_end(server_id, token.clone(), cx);
self.broadcast_language_server_update( self.buffer_ordered_messages_tx
server_id, .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { language_server_id: server_id,
token, message: proto::update_language_server::Variant::WorkEnd(
}), proto::LspWorkEnd { token },
); ),
})
.ok();
} }
} }
} }
@ -2703,22 +2785,6 @@ impl Project {
}) })
} }
fn broadcast_language_server_update(
&self,
language_server_id: usize,
event: proto::update_language_server::Variant,
) {
if let Some(project_id) = self.remote_id() {
self.client
.send(proto::UpdateLanguageServer {
project_id,
language_server_id: language_server_id as u64,
variant: Some(event),
})
.log_err();
}
}
pub fn language_server_statuses( pub fn language_server_statuses(
&self, &self,
) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> { ) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
@ -4727,8 +4793,8 @@ impl Project {
if is_host { if is_host {
this.opened_buffers this.opened_buffers
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_))); .retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
this.buffer_changes_tx this.buffer_ordered_messages_tx
.unbounded_send(BufferMessage::Resync) .unbounded_send(BufferOrderedMessage::Resync)
.unwrap(); .unwrap();
} }

View file

@ -2183,7 +2183,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
}); });
} }
#[gpui::test] #[gpui::test(iterations = 10)]
async fn test_save_file(cx: &mut gpui::TestAppContext) { async fn test_save_file(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(

File diff suppressed because it is too large Load diff

View file

@ -28,11 +28,11 @@ pub use watched_json::watch_files;
#[derive(Clone)] #[derive(Clone)]
pub struct Settings { pub struct Settings {
pub features: Features,
pub buffer_font_family_name: String, pub buffer_font_family_name: String,
pub buffer_font_features: fonts::Features, pub buffer_font_features: fonts::Features,
pub buffer_font_family: FamilyId, pub buffer_font_family: FamilyId,
pub default_buffer_font_size: f32, pub default_buffer_font_size: f32,
pub enable_copilot_integration: bool,
pub buffer_font_size: f32, pub buffer_font_size: f32,
pub active_pane_magnification: f32, pub active_pane_magnification: f32,
pub cursor_blink: bool, pub cursor_blink: bool,
@ -177,43 +177,7 @@ pub struct EditorSettings {
pub ensure_final_newline_on_save: Option<bool>, pub ensure_final_newline_on_save: Option<bool>,
pub formatter: Option<Formatter>, pub formatter: Option<Formatter>,
pub enable_language_server: Option<bool>, pub enable_language_server: Option<bool>,
#[schemars(skip)] pub show_copilot_suggestions: Option<bool>,
pub copilot: Option<OnOff>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OnOff {
On,
Off,
}
impl OnOff {
pub fn as_bool(&self) -> bool {
match self {
OnOff::On => true,
OnOff::Off => false,
}
}
pub fn from_bool(value: bool) -> OnOff {
match value {
true => OnOff::On,
false => OnOff::Off,
}
}
}
impl From<OnOff> for bool {
fn from(value: OnOff) -> bool {
value.as_bool()
}
}
impl From<bool> for OnOff {
fn from(value: bool) -> OnOff {
OnOff::from_bool(value)
}
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -437,8 +401,7 @@ pub struct SettingsFileContent {
#[serde(default)] #[serde(default)]
pub base_keymap: Option<BaseKeymap>, pub base_keymap: Option<BaseKeymap>,
#[serde(default)] #[serde(default)]
#[schemars(skip)] pub features: FeaturesContent,
pub enable_copilot_integration: Option<bool>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -447,6 +410,18 @@ pub struct LspSettings {
pub initialization_options: Option<Value>, pub initialization_options: Option<Value>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct Features {
pub copilot: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct FeaturesContent {
pub copilot: Option<bool>,
}
impl Settings { impl Settings {
/// Fill out the settings corresponding to the default.json file, overrides will be set later /// Fill out the settings corresponding to the default.json file, overrides will be set later
pub fn defaults( pub fn defaults(
@ -500,7 +475,7 @@ impl Settings {
format_on_save: required(defaults.editor.format_on_save), format_on_save: required(defaults.editor.format_on_save),
formatter: required(defaults.editor.formatter), formatter: required(defaults.editor.formatter),
enable_language_server: required(defaults.editor.enable_language_server), enable_language_server: required(defaults.editor.enable_language_server),
copilot: required(defaults.editor.copilot), show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
}, },
editor_overrides: Default::default(), editor_overrides: Default::default(),
git: defaults.git.unwrap(), git: defaults.git.unwrap(),
@ -517,7 +492,9 @@ impl Settings {
telemetry_overrides: Default::default(), telemetry_overrides: Default::default(),
auto_update: defaults.auto_update.unwrap(), auto_update: defaults.auto_update.unwrap(),
base_keymap: Default::default(), base_keymap: Default::default(),
enable_copilot_integration: defaults.enable_copilot_integration.unwrap(), features: Features {
copilot: defaults.features.copilot.unwrap(),
},
} }
} }
@ -569,10 +546,7 @@ impl Settings {
merge(&mut self.autosave, data.autosave); merge(&mut self.autosave, data.autosave);
merge(&mut self.default_dock_anchor, data.default_dock_anchor); merge(&mut self.default_dock_anchor, data.default_dock_anchor);
merge(&mut self.base_keymap, data.base_keymap); merge(&mut self.base_keymap, data.base_keymap);
merge( merge(&mut self.features.copilot, data.features.copilot);
&mut self.enable_copilot_integration,
data.enable_copilot_integration,
);
self.editor_overrides = data.editor; self.editor_overrides = data.editor;
self.git_overrides = data.git.unwrap_or_default(); self.git_overrides = data.git.unwrap_or_default();
@ -596,12 +570,15 @@ impl Settings {
self self
} }
pub fn copilot_on(&self, language: Option<&str>) -> bool { pub fn features(&self) -> &Features {
if self.enable_copilot_integration { &self.features
self.language_setting(language, |settings| settings.copilot.map(Into::into)) }
} else {
false pub fn show_copilot_suggestions(&self, language: Option<&str>) -> bool {
} self.features.copilot
&& self.language_setting(language, |settings| {
settings.show_copilot_suggestions.map(Into::into)
})
} }
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 { pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
@ -740,7 +717,7 @@ impl Settings {
format_on_save: Some(FormatOnSave::On), format_on_save: Some(FormatOnSave::On),
formatter: Some(Formatter::LanguageServer), formatter: Some(Formatter::LanguageServer),
enable_language_server: Some(true), enable_language_server: Some(true),
copilot: Some(OnOff::On), show_copilot_suggestions: Some(true),
}, },
editor_overrides: Default::default(), editor_overrides: Default::default(),
journal_defaults: Default::default(), journal_defaults: Default::default(),
@ -760,7 +737,7 @@ impl Settings {
telemetry_overrides: Default::default(), telemetry_overrides: Default::default(),
auto_update: true, auto_update: true,
base_keymap: Default::default(), base_keymap: Default::default(),
enable_copilot_integration: true, features: Features { copilot: true },
} }
} }
@ -1125,7 +1102,7 @@ mod tests {
{ {
"language_overrides": { "language_overrides": {
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
@ -1135,7 +1112,7 @@ mod tests {
settings.languages.insert( settings.languages.insert(
"Rust".into(), "Rust".into(),
EditorSettings { EditorSettings {
copilot: Some(OnOff::On), show_copilot_suggestions: Some(true),
..Default::default() ..Default::default()
}, },
); );
@ -1144,10 +1121,10 @@ mod tests {
{ {
"language_overrides": { "language_overrides": {
"Rust": { "Rust": {
"copilot": "on" "show_copilot_suggestions": true
}, },
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
@ -1163,21 +1140,21 @@ mod tests {
{ {
"languages": { "languages": {
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
"# "#
.unindent(), .unindent(),
|settings| { |settings| {
settings.editor.copilot = Some(OnOff::On); settings.editor.show_copilot_suggestions = Some(true);
}, },
r#" r#"
{ {
"copilot": "on", "show_copilot_suggestions": true,
"languages": { "languages": {
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
@ -1187,13 +1164,13 @@ mod tests {
} }
#[test] #[test]
fn test_update_langauge_copilot() { fn test_update_language_copilot() {
assert_new_settings( assert_new_settings(
r#" r#"
{ {
"languages": { "languages": {
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
@ -1203,7 +1180,7 @@ mod tests {
settings.languages.insert( settings.languages.insert(
"Rust".into(), "Rust".into(),
EditorSettings { EditorSettings {
copilot: Some(OnOff::On), show_copilot_suggestions: Some(true),
..Default::default() ..Default::default()
}, },
); );
@ -1212,10 +1189,10 @@ mod tests {
{ {
"languages": { "languages": {
"Rust": { "Rust": {
"copilot": "on" "show_copilot_suggestions": true
}, },
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }

View file

@ -154,6 +154,12 @@ impl<K> TreeSet<K>
where where
K: Clone + Debug + Default + Ord, K: Clone + Debug + Default + Ord,
{ {
pub fn from_ordered_entries(entries: impl IntoIterator<Item = K>) -> Self {
Self(TreeMap::from_ordered_entries(
entries.into_iter().map(|key| (key, ())),
))
}
pub fn insert(&mut self, key: K) { pub fn insert(&mut self, key: K) {
self.0.insert(key, ()); self.0.insert(key, ());
} }

View file

@ -1331,15 +1331,15 @@ impl Buffer {
} }
} }
pub fn wait_for_anchors<'a>( pub fn wait_for_anchors(
&mut self, &mut self,
anchors: impl IntoIterator<Item = &'a Anchor>, anchors: impl IntoIterator<Item = Anchor>,
) -> impl 'static + Future<Output = Result<()>> { ) -> impl 'static + Future<Output = Result<()>> {
let mut futures = Vec::new(); let mut futures = Vec::new();
for anchor in anchors { for anchor in anchors {
if !self.version.observed(anchor.timestamp) if !self.version.observed(anchor.timestamp)
&& *anchor != Anchor::MAX && anchor != Anchor::MAX
&& *anchor != Anchor::MIN && anchor != Anchor::MIN
{ {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.edit_id_resolvers self.edit_id_resolvers

View file

@ -785,6 +785,10 @@ impl Pane {
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
let pane_handle = workspace.active_pane().clone(); let pane_handle = workspace.active_pane().clone();
let pane = pane_handle.read(cx); let pane = pane_handle.read(cx);
if pane.items.is_empty() {
return None;
}
let active_item_id = pane.items[pane.active_item_index].id(); let active_item_id = pane.items[pane.active_item_index].id();
let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx); let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx);
@ -2078,6 +2082,19 @@ mod tests {
use gpui::{executor::Deterministic, TestAppContext}; use gpui::{executor::Deterministic, TestAppContext};
use project::FakeFs; use project::FakeFs;
#[gpui::test]
async fn test_remove_active_empty(cx: &mut TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
workspace.update(cx, |workspace, cx| {
assert!(Pane::close_active_item(workspace, &CloseActiveItem, cx).is_none())
});
}
#[gpui::test] #[gpui::test]
async fn test_add_item_with_new_item(cx: &mut TestAppContext) { async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor." description = "The fast, collaborative code editor."
edition = "2021" edition = "2021"
name = "zed" name = "zed"
version = "0.82.0" version = "0.82.9"
publish = false publish = false
[lib] [lib]

View file

@ -1 +1 @@
dev stable

View file

@ -44,9 +44,7 @@ export default function editor(colorScheme: ColorScheme) {
activeLineBackground: withOpacity(background(layer, "on"), 0.75), activeLineBackground: withOpacity(background(layer, "on"), 0.75),
highlightedLineBackground: background(layer, "on"), highlightedLineBackground: background(layer, "on"),
// Inline autocomplete suggestions, Co-pilot suggestions, etc. // Inline autocomplete suggestions, Co-pilot suggestions, etc.
suggestion: { suggestion: syntax.predictive,
color: syntax.predictive.color,
},
codeActions: { codeActions: {
indicator: { indicator: {
color: foreground(layer, "variant"), color: foreground(layer, "variant"),

View file

@ -1,6 +1,7 @@
import deepmerge from "deepmerge" import deepmerge from "deepmerge"
import { FontWeight, fontWeights } from "../../common" import { FontWeight, fontWeights } from "../../common"
import { ColorScheme } from "./colorScheme" import { ColorScheme } from "./colorScheme"
import chroma from "chroma-js"
export interface SyntaxHighlightStyle { export interface SyntaxHighlightStyle {
color: string color: string
@ -128,6 +129,8 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
[key: string]: Omit<SyntaxHighlightStyle, "color"> [key: string]: Omit<SyntaxHighlightStyle, "color">
} = {} } = {}
const light = colorScheme.isLight
// then spread the default to each style // then spread the default to each style
for (const key of Object.keys({} as Syntax)) { for (const key of Object.keys({} as Syntax)) {
syntax[key as keyof Syntax] = { syntax[key as keyof Syntax] = {
@ -135,11 +138,20 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
} }
} }
// Mix the neutral and blue colors to get a
// predictive color distinct from any other color in the theme
const predictive = chroma.mix(
colorScheme.ramps.neutral(0.4).hex(),
colorScheme.ramps.blue(0.4).hex(),
0.45,
"lch"
).hex()
const color = { const color = {
primary: colorScheme.ramps.neutral(1).hex(), primary: colorScheme.ramps.neutral(1).hex(),
comment: colorScheme.ramps.neutral(0.71).hex(), comment: colorScheme.ramps.neutral(0.71).hex(),
punctuation: colorScheme.ramps.neutral(0.86).hex(), punctuation: colorScheme.ramps.neutral(0.86).hex(),
predictive: colorScheme.ramps.neutral(0.57).hex(), predictive: predictive,
emphasis: colorScheme.ramps.blue(0.5).hex(), emphasis: colorScheme.ramps.blue(0.5).hex(),
string: colorScheme.ramps.orange(0.5).hex(), string: colorScheme.ramps.orange(0.5).hex(),
function: colorScheme.ramps.yellow(0.5).hex(), function: colorScheme.ramps.yellow(0.5).hex(),
@ -169,6 +181,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
}, },
predictive: { predictive: {
color: color.predictive, color: color.predictive,
italic: true,
}, },
emphasis: { emphasis: {
color: color.emphasis, color: color.emphasis,