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",
"theme",
"util",
"workspace",
]
[[package]]
@ -8516,7 +8515,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.82.0"
version = "0.82.9"
dependencies = [
"activity_indicator",
"anyhow",

View file

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

View file

@ -1,6 +1,11 @@
{
// The name of the Zed theme to use for the UI
"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
"buffer_font_family": "Zed Mono",
// 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
// which gives the same size as all other panes.
"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
"vim_mode": false,
// 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
// explicitly requesting it.
"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.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.

View file

@ -29,7 +29,10 @@ use std::{
env, future, mem,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use unindent::Unindent as _;
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)]
async fn test_collaborating_with_completion(
deterministic: Arc<Deterministic>,
@ -5862,10 +6000,17 @@ async fn test_basic_following(
// Client A updates their selections in those editors
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.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.
@ -5878,6 +6023,27 @@ async fn test_basic_following(
.await
.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();
let active_call_c = cx_c.read(ActiveCall::global);
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.
workspace_a.update(cx_a, |workspace, 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"] }
lsp = { path = "../lsp", 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,
};
use util::{
channel::ReleaseChannel, fs::remove_matching, github::latest_github_release, http::HttpClient,
paths, ResultExt,
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
};
const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
actions!(copilot_auth, [SignIn, SignOut]);
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) {
// 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 node_runtime = node_runtime.clone();
move |cx| Copilot::start(http, node_runtime, cx)
@ -172,7 +165,7 @@ impl Copilot {
let http = http.clone();
let node_runtime = node_runtime.clone();
move |this, cx| {
if cx.global::<Settings>().enable_copilot_integration {
if cx.global::<Settings>().features.copilot {
if matches!(this.server, CopilotServer::Disabled) {
let start_task = cx
.spawn({
@ -194,12 +187,14 @@ impl Copilot {
})
.detach();
if cx.global::<Settings>().enable_copilot_integration {
if cx.global::<Settings>().features.copilot {
let start_task = cx
.spawn({
let http = http.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();
@ -254,13 +249,6 @@ impl Copilot {
cx.clone(),
)?;
let server = server.initialize(Default::default()).await?;
let status = server
.request::<request::CheckStatus>(request::CheckStatusParams {
local_checks_only: false,
})
.await?;
server
.on_notification::<LogMessage, _>(|params, _cx| {
match params.level {
@ -281,6 +269,13 @@ impl Copilot {
)
.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))
};

View file

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

View file

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

View file

@ -24,6 +24,15 @@ const COPILOT_ERROR_TOAST_ID: usize = 1338;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotMenu;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotStartMenu;
#[derive(Clone, PartialEq)]
pub struct HideCopilot;
#[derive(Clone, PartialEq)]
pub struct InitiateSignIn;
#[derive(Clone, PartialEq)]
pub struct ToggleCopilotForLanguage {
language: Arc<str>,
@ -40,6 +49,9 @@ impl_internal_actions!(
copilot,
[
DeployCopilotMenu,
DeployCopilotStartMenu,
HideCopilot,
InitiateSignIn,
DeployCopilotModal,
ToggleCopilotForLanguage,
ToggleCopilotGlobally,
@ -48,17 +60,19 @@ impl_internal_actions!(
pub fn init(cx: &mut AppContext) {
cx.add_action(CopilotButton::deploy_copilot_menu);
cx.add_action(CopilotButton::deploy_copilot_start_menu);
cx.add_action(
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
let language = action.language.to_owned();
let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
let language = action.language.clone();
let show_copilot_suggestions = cx
.global::<Settings>()
.show_copilot_suggestions(Some(&language));
SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert(
language.to_owned(),
language,
settings::EditorSettings {
copilot: Some((!current_langauge).into()),
show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
..Default::default()
},
);
@ -67,12 +81,63 @@ pub fn init(cx: &mut AppContext) {
);
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| {
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 {
@ -94,7 +159,7 @@ impl View for CopilotButton {
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
let settings = cx.global::<Settings>();
if !settings.enable_copilot_integration {
if !settings.features.copilot {
return Empty::new().boxed();
}
@ -105,9 +170,9 @@ impl View for CopilotButton {
};
let status = copilot.read(cx).status();
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
let view_id = cx.view_id();
let enabled = self
.editor_enabled
.unwrap_or(settings.show_copilot_suggestions(None));
Stack::new()
.with_child(
@ -155,48 +220,13 @@ impl View for CopilotButton {
let status = status.clone();
move |_, cx| match status {
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(
COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e),
"Reinstall Copilot",
Reinstall,
)),
_ => cx.dispatch_action(SignIn),
_ => cx.dispatch_action(DeployCopilotStartMenu),
}
})
.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>) {
let settings = cx.global::<Settings>();
let mut menu_options = Vec::with_capacity(6);
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(
format!(
"{} Copilot for {}",
if language_enabled {
"Disable"
} else {
"Enable"
},
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language
),
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(
if globally_enabled {
"Disable Copilot Globally"
"Hide Suggestions for All Files"
} else {
"Enable Copilot Globally"
"Show Suggestions for All Files"
},
ToggleCopilotGlobally,
));
@ -319,7 +365,7 @@ impl CopilotButton {
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()
}

View file

@ -20,7 +20,7 @@ mod editor_tests;
pub mod test;
use aho_corasick::AhoCorasick;
use anyhow::Result;
use anyhow::{anyhow, Result};
use blink_manager::BlinkManager;
use clock::ReplicaId;
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_action(Editor::next_copilot_suggestion);
cx.add_action(Editor::previous_copilot_suggestion);
cx.add_action(Editor::copilot_suggest);
hover_popover::init(cx);
link_go_to_definition::init(cx);
@ -1014,6 +1015,8 @@ impl CodeActionsMenu {
pub struct CopilotState {
excerpt_id: Option<ExcerptId>,
pending_refresh: Task<Option<()>>,
pending_cycling_refresh: Task<Option<()>>,
cycled: bool,
completions: Vec<copilot::Completion>,
active_completion_index: usize,
}
@ -1022,9 +1025,11 @@ impl Default for CopilotState {
fn default() -> Self {
Self {
excerpt_id: None,
pending_cycling_refresh: Task::ready(Some(())),
pending_refresh: Task::ready(Some(())),
completions: Default::default(),
active_completion_index: 0,
cycled: false,
}
}
}
@ -1040,7 +1045,8 @@ impl CopilotState {
let completion = self.completions.get(self.active_completion_index)?;
let excerpt_id = self.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)
{
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) {
for completion in &self.completions {
if *completion == new_completion {
@ -1264,7 +1290,7 @@ impl Editor {
cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed),
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);
@ -2025,13 +2051,13 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
if had_active_copilot_suggestion {
this.refresh_copilot_suggestions(cx);
this.refresh_copilot_suggestions(true, cx);
if !this.has_active_copilot_suggestion(cx) {
this.trigger_completion_on_input(&text, cx);
}
} else {
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();
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 task = cx.spawn_weak(|this, mut cx| {
async move {
let completions = completions.await?;
if completions.is_empty() {
return Ok(());
}
let mut menu = CompletionsMenu {
id,
initial_position: position,
match_candidates: completions
.iter()
.enumerate()
.map(|(id, completion)| {
StringMatchCandidate::new(
id,
completion.label.text[completion.label.filter_range.clone()].into(),
)
})
.collect(),
buffer,
completions: completions.into(),
matches: Vec::new().into(),
selected_item: 0,
list: Default::default(),
let menu = if let Some(completions) = completions.await.log_err() {
let mut menu = CompletionsMenu {
id,
initial_position: position,
match_candidates: completions
.iter()
.enumerate()
.map(|(id, completion)| {
StringMatchCandidate::new(
id,
completion.label.text[completion.label.filter_range.clone()]
.into(),
)
})
.collect(),
buffer,
completions: completions.into(),
matches: Vec::new().into(),
selected_item: 0,
list: Default::default(),
};
menu.filter(query.as_deref(), cx.background()).await;
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) {
this.update(&mut cx, |this, cx| {
match this.context_menu.as_ref() {
None => {}
Some(ContextMenu::Completions(prev_menu)) => {
if prev_menu.id > menu.id {
return;
}
match this.context_menu.as_ref() {
None => {}
Some(ContextMenu::Completions(prev_menu)) => {
if prev_menu.id > id {
return;
}
_ => return,
}
_ => return,
}
this.completion_tasks.retain(|(id, _)| *id > menu.id);
if this.focused && !menu.matches.is_empty() {
this.show_context_menu(ContextMenu::Completions(menu), cx);
} else if this.hide_context_menu(cx).is_none() {
if this.focused && menu.is_some() {
let menu = menu.unwrap();
this.show_context_menu(ContextMenu::Completions(menu), cx);
} 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);
}
});
}
}
});
Ok::<_, anyhow::Error>(())
}
.log_err()
@ -2498,7 +2537,7 @@ impl Editor {
});
}
this.refresh_copilot_suggestions(cx);
this.refresh_copilot_suggestions(true, cx);
});
let project = self.project.clone()?;
@ -2791,10 +2830,14 @@ impl Editor {
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)?;
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
self.hide_copilot_suggestion(cx);
self.clear_copilot_suggestions(cx);
return None;
}
self.update_visible_copilot_suggestion(cx);
@ -2802,28 +2845,35 @@ impl Editor {
let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor = self.selections.newest_anchor().head();
let language_name = snapshot.language_at(cursor).map(|language| language.name());
if !cx.global::<Settings>().copilot_on(language_name.as_deref()) {
self.hide_copilot_suggestion(cx);
if !cx
.global::<Settings>()
.show_copilot_suggestions(language_name.as_deref())
{
self.clear_copilot_suggestions(cx);
return None;
}
let (buffer, buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
(
copilot.completions(&buffer, buffer_position, cx),
copilot.completions_cycling(&buffer, buffer_position, cx),
)
});
if debounce {
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
}
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| {
if !completions.is_empty() {
this.copilot_state.cycled = false;
this.copilot_state.pending_cycling_refresh = Task::ready(None);
this.copilot_state.completions.clear();
this.copilot_state.active_completion_index = 0;
this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
@ -2840,34 +2890,73 @@ impl Editor {
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) {
self.refresh_copilot_suggestions(cx);
self.refresh_copilot_suggestions(false, cx);
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);
}
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(
&mut self,
_: &copilot::PreviousSuggestion,
cx: &mut ViewContext<Self>,
) {
if !self.has_active_copilot_suggestion(cx) {
self.refresh_copilot_suggestions(cx);
return;
if self.has_active_copilot_suggestion(cx) {
self.cycle_suggestions(Direction::Prev, cx);
} 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 {
@ -2909,11 +2998,11 @@ impl Editor {
.copilot_state
.text_for_active_completion(cursor, &snapshot)
{
self.display_map.update(cx, |map, cx| {
self.display_map.update(cx, move |map, cx| {
map.replace_suggestion(
Some(Suggestion {
position: cursor,
text: text.into(),
text: text.trim_end().into(),
}),
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(
&self,
style: &EditorStyle,
@ -3209,7 +3303,7 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.insert("", cx);
this.refresh_copilot_suggestions(cx);
this.refresh_copilot_suggestions(true, cx);
});
}
@ -3225,7 +3319,7 @@ impl Editor {
})
});
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| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.refresh_copilot_suggestions(cx);
this.refresh_copilot_suggestions(true, cx);
});
}
@ -4001,7 +4095,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_copilot_suggestions(cx);
self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited);
}
}
@ -4016,7 +4110,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_copilot_suggestions(cx);
self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited);
}
}
@ -6477,8 +6571,8 @@ impl Editor {
cx.notify();
}
fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(cx);
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(true, cx);
}
pub fn set_searchable(&mut self, searchable: bool) {
@ -6619,13 +6713,15 @@ impl Editor {
.as_singleton()
.and_then(|b| b.read(cx).file()),
) {
let settings = cx.global::<Settings>();
let extension = Path::new(file.file_name(cx))
.extension()
.and_then(|e| e.to_str());
project.read(cx).client().report_event(
name,
json!({ "File Extension": extension }),
cx.global::<Settings>().telemetry(),
json!({ "File Extension": extension, "Vim Mode": settings.vim_mode }),
settings.telemetry(),
);
}
}

View file

@ -4322,7 +4322,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
cx.set_state(
&[
"one ", //
"twoˇ", //
"twoˇ", //
"three ", //
"four", //
]
@ -4397,7 +4397,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
&[
"one", //
"", //
"twoˇ", //
"twoˇ", //
"", //
"three", //
"four", //
@ -4412,7 +4412,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
cx.assert_editor_state(
&[
"one ", //
"twoˇ", //
"twoˇ", //
"three ", //
"four", //
]
@ -5897,13 +5897,12 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
)
.await;
// When inserting, ensure autocompletion is favored over Copilot suggestions.
cx.set_state(indoc! {"
oneˇ
two
three
"});
// When inserting, ensure autocompletion is favored over Copilot suggestions.
cx.simulate_keystroke(".");
let _ = handle_completion_request(
&mut cx,
@ -5917,8 +5916,8 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
text: "copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
}],
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");
});
// Ensure Copilot suggestions are shown right away if no autocompletion is available.
cx.set_state(indoc! {"
oneˇ
two
three
"});
cx.simulate_keystroke(".");
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three
"},
vec![],
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
}],
vec![],
);
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(".");
let _ = handle_completion_request(
&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> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point

View file

@ -3,12 +3,12 @@ use crate::{
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, RenderContext,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{
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 mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {
editor.remote_id(&client, cx) == Some(remote_id)
|| state.singleton
&& buffers.len() == 1
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref()
== Some(&buffers[0])
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
let singleton_buffer_matches = state.singleton
&& buffers.first()
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
ids_match || singleton_buffer_matches
})
});
@ -115,46 +115,29 @@ impl FollowableItem for Editor {
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| {
editor.remote_id = Some(remote_id);
let buffer = editor.buffer.read(cx).read(cx);
let selections = state
.selections
.into_iter()
.map(|selection| {
deserialize_selection(&buffer, selection)
.ok_or_else(|| anyhow!("invalid selection"))
})
.collect::<Result<Vec<_>>>()?;
let pending_selection = state
.pending_selection
.map(|selection| deserialize_selection(&buffer, selection))
.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(())
})?;
update_editor_from_message(
editor.clone(),
project,
proto::update_view::Editor {
selections: state.selections,
pending_selection: state.pending_selection,
scroll_top_anchor: state.scroll_top_anchor,
scroll_x: state.scroll_x,
scroll_y: state.scroll_y,
..Default::default()
},
&mut cx,
)
.await?;
Ok(editor)
}))
@ -299,96 +282,9 @@ impl FollowableItem for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
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();
cx.spawn(|this, mut cx| async move {
let _buffers = try_join_all(buffers).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(())
update_editor_from_message(this, project, message, &mut cx).await
})
}
@ -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(
buffer_id: u64,
id: &ExcerptId,

View file

@ -1,6 +1,7 @@
mod anchor;
pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::{anyhow, Result};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt};
@ -16,7 +17,9 @@ use language::{
use std::{
borrow::Cow,
cell::{Ref, RefCell},
cmp, fmt, io,
cmp, fmt,
future::Future,
io,
iter::{self, FromIterator},
mem,
ops::{Range, RangeBounds, Sub},
@ -1238,6 +1241,39 @@ impl MultiBuffer {
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>(
&self,
position: T,

View file

@ -523,31 +523,7 @@ impl FakeFs {
}
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
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(())
})
.unwrap();
state.emit_event(&[path]);
self.write_file_internal(path, content).unwrap()
}
pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
@ -569,6 +545,33 @@ impl FakeFs {
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) {
self.state.lock().events_paused = true;
}
@ -952,7 +955,7 @@ impl Fs for FakeFs {
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
self.simulate_random_delay().await;
let path = normalize_path(path.as_path());
self.insert_file(path, data.to_string()).await;
self.write_file_internal(path, data.to_string())?;
Ok(())
}
@ -961,7 +964,7 @@ impl Fs for FakeFs {
self.simulate_random_delay().await;
let path = normalize_path(path);
let content = chunks(text, line_ending).collect();
self.insert_file(path, content).await;
self.write_file_internal(path, content)?;
Ok(())
}

View file

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

View file

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

View file

@ -92,7 +92,7 @@ pub trait Item {
pub struct Project {
worktrees: Vec<WorktreeHandle>,
active_entry: Option<ProjectEntryId>,
buffer_changes_tx: mpsc::UnboundedSender<BufferMessage>,
buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>,
languages: Arc<LanguageRegistry>,
language_servers: HashMap<usize, LanguageServerState>,
language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>,
@ -131,11 +131,16 @@ pub struct Project {
terminals: Terminals,
}
enum BufferMessage {
/// Message ordered with respect to buffer operations
enum BufferOrderedMessage {
Operation {
buffer_id: u64,
operation: proto::Operation,
},
LanguageServerUpdate {
language_server_id: usize,
message: proto::update_language_server::Variant,
},
Resync,
}
@ -436,11 +441,11 @@ impl Project {
) -> ModelHandle<Self> {
cx.add_model(|cx: &mut ModelContext<Self>| {
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();
Self {
worktrees: Default::default(),
buffer_changes_tx: tx,
buffer_ordered_messages_tx: tx,
collaborators: Default::default(),
opened_buffers: Default::default(),
shared_buffers: Default::default(),
@ -504,11 +509,11 @@ impl Project {
}
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();
let mut this = Self {
worktrees: Vec::new(),
buffer_changes_tx: tx,
buffer_ordered_messages_tx: tx,
loading_buffers_by_path: Default::default(),
opened_buffer: watch::channel(),
shared_buffers: Default::default(),
@ -1152,8 +1157,8 @@ impl Project {
)
})
.collect();
self.buffer_changes_tx
.unbounded_send(BufferMessage::Resync)
self.buffer_ordered_messages_tx
.unbounded_send(BufferOrderedMessage::Resync)
.unwrap();
cx.notify();
Ok(())
@ -1731,38 +1736,64 @@ impl Project {
});
}
async fn send_buffer_messages(
async fn send_buffer_ordered_messages(
this: WeakModelHandle<Self>,
mut rx: UnboundedReceiver<BufferMessage>,
rx: UnboundedReceiver<BufferOrderedMessage>,
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;
while let Some(change) = rx.next().await {
if let Some(this) = this.upgrade(&mut cx) {
let is_local = this.read_with(&cx, |this, _| this.is_local());
let mut changes = rx.ready_chunks(MAX_BATCH_SIZE);
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 {
BufferMessage::Operation {
BufferOrderedMessage::Operation {
buffer_id,
operation,
} => {
if needs_resync_with_host {
continue;
}
let request = this.read_with(&cx, |this, _| {
let project_id = this.remote_id()?;
Some(this.client.request(proto::UpdateBuffer {
buffer_id,
project_id,
operations: vec![operation],
}))
});
if let Some(request) = request {
if request.await.is_err() && !is_local {
needs_resync_with_host = true;
}
}
operations_by_buffer_id
.entry(buffer_id)
.or_insert(Vec::new())
.push(operation);
}
BufferMessage::Resync => {
BufferOrderedMessage::Resync => {
operations_by_buffer_id.clear();
if this
.update(&mut cx, |this, cx| this.synchronize_remote_buffers(cx))
.await
@ -1771,11 +1802,46 @@ impl Project {
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(
@ -1786,8 +1852,8 @@ impl Project {
) -> Option<()> {
match event {
BufferEvent::Operation(operation) => {
self.buffer_changes_tx
.unbounded_send(BufferMessage::Operation {
self.buffer_ordered_messages_tx
.unbounded_send(BufferOrderedMessage::Operation {
buffer_id: buffer.read(cx).remote_id(),
operation: language::proto::serialize_operation(operation),
})
@ -1878,14 +1944,19 @@ impl Project {
let task = cx.spawn_weak(|this, mut cx| async move {
cx.background().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx | {
this.disk_based_diagnostics_finished(language_server_id, cx);
this.broadcast_language_server_update(
this.update(&mut cx, |this, cx| {
this.disk_based_diagnostics_finished(
language_server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {},
),
cx,
);
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 {
language_server_status.has_pending_diagnostic_updates = true;
self.disk_based_diagnostics_started(server_id, cx);
self.broadcast_language_server_update(
server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
proto::LspDiskBasedDiagnosticsUpdating {},
),
);
self.buffer_ordered_messages_tx
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
language_server_id: server_id,
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(Default::default())
})
.ok();
} else {
self.on_lsp_work_start(
server_id,
@ -2541,14 +2612,18 @@ impl Project {
},
cx,
);
self.broadcast_language_server_update(
server_id,
proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
token,
message: report.message,
percentage: report.percentage.map(|p| p as u32),
}),
);
self.buffer_ordered_messages_tx
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
language_server_id: server_id,
message: proto::update_language_server::Variant::WorkStart(
proto::LspWorkStart {
token,
message: report.message,
percentage: report.percentage.map(|p| p as u32),
},
),
})
.ok();
}
}
lsp::WorkDoneProgress::Report(report) => {
@ -2563,16 +2638,18 @@ impl Project {
},
cx,
);
self.broadcast_language_server_update(
server_id,
proto::update_language_server::Variant::WorkProgress(
proto::LspWorkProgress {
token,
message: report.message,
percentage: report.percentage.map(|p| p as u32),
},
),
);
self.buffer_ordered_messages_tx
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
language_server_id: server_id,
message: proto::update_language_server::Variant::WorkProgress(
proto::LspWorkProgress {
token,
message: report.message,
percentage: report.percentage.map(|p| p as u32),
},
),
})
.ok();
}
}
lsp::WorkDoneProgress::End(_) => {
@ -2581,20 +2658,25 @@ impl Project {
if is_disk_based_diagnostics_progress {
language_server_status.has_pending_diagnostic_updates = false;
self.disk_based_diagnostics_finished(server_id, cx);
self.broadcast_language_server_update(
server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {},
),
);
self.buffer_ordered_messages_tx
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
language_server_id: server_id,
message:
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
Default::default(),
),
})
.ok();
} else {
self.on_lsp_work_end(server_id, token.clone(), cx);
self.broadcast_language_server_update(
server_id,
proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
token,
}),
);
self.buffer_ordered_messages_tx
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
language_server_id: server_id,
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(
&self,
) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
@ -4727,8 +4793,8 @@ impl Project {
if is_host {
this.opened_buffers
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
this.buffer_changes_tx
.unbounded_send(BufferMessage::Resync)
this.buffer_ordered_messages_tx
.unbounded_send(BufferOrderedMessage::Resync)
.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) {
let fs = FakeFs::new(cx.background());
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)]
pub struct Settings {
pub features: Features,
pub buffer_font_family_name: String,
pub buffer_font_features: fonts::Features,
pub buffer_font_family: FamilyId,
pub default_buffer_font_size: f32,
pub enable_copilot_integration: bool,
pub buffer_font_size: f32,
pub active_pane_magnification: f32,
pub cursor_blink: bool,
@ -177,43 +177,7 @@ pub struct EditorSettings {
pub ensure_final_newline_on_save: Option<bool>,
pub formatter: Option<Formatter>,
pub enable_language_server: Option<bool>,
#[schemars(skip)]
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)
}
pub show_copilot_suggestions: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -437,8 +401,7 @@ pub struct SettingsFileContent {
#[serde(default)]
pub base_keymap: Option<BaseKeymap>,
#[serde(default)]
#[schemars(skip)]
pub enable_copilot_integration: Option<bool>,
pub features: FeaturesContent,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -447,6 +410,18 @@ pub struct LspSettings {
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 {
/// Fill out the settings corresponding to the default.json file, overrides will be set later
pub fn defaults(
@ -500,7 +475,7 @@ impl Settings {
format_on_save: required(defaults.editor.format_on_save),
formatter: required(defaults.editor.formatter),
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(),
git: defaults.git.unwrap(),
@ -517,7 +492,9 @@ impl Settings {
telemetry_overrides: Default::default(),
auto_update: defaults.auto_update.unwrap(),
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.default_dock_anchor, data.default_dock_anchor);
merge(&mut self.base_keymap, data.base_keymap);
merge(
&mut self.enable_copilot_integration,
data.enable_copilot_integration,
);
merge(&mut self.features.copilot, data.features.copilot);
self.editor_overrides = data.editor;
self.git_overrides = data.git.unwrap_or_default();
@ -596,12 +570,15 @@ impl Settings {
self
}
pub fn copilot_on(&self, language: Option<&str>) -> bool {
if self.enable_copilot_integration {
self.language_setting(language, |settings| settings.copilot.map(Into::into))
} else {
false
}
pub fn features(&self) -> &Features {
&self.features
}
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 {
@ -740,7 +717,7 @@ impl Settings {
format_on_save: Some(FormatOnSave::On),
formatter: Some(Formatter::LanguageServer),
enable_language_server: Some(true),
copilot: Some(OnOff::On),
show_copilot_suggestions: Some(true),
},
editor_overrides: Default::default(),
journal_defaults: Default::default(),
@ -760,7 +737,7 @@ impl Settings {
telemetry_overrides: Default::default(),
auto_update: true,
base_keymap: Default::default(),
enable_copilot_integration: true,
features: Features { copilot: true },
}
}
@ -1125,7 +1102,7 @@ mod tests {
{
"language_overrides": {
"JSON": {
"copilot": "off"
"show_copilot_suggestions": false
}
}
}
@ -1135,7 +1112,7 @@ mod tests {
settings.languages.insert(
"Rust".into(),
EditorSettings {
copilot: Some(OnOff::On),
show_copilot_suggestions: Some(true),
..Default::default()
},
);
@ -1144,10 +1121,10 @@ mod tests {
{
"language_overrides": {
"Rust": {
"copilot": "on"
"show_copilot_suggestions": true
},
"JSON": {
"copilot": "off"
"show_copilot_suggestions": false
}
}
}
@ -1163,21 +1140,21 @@ mod tests {
{
"languages": {
"JSON": {
"copilot": "off"
"show_copilot_suggestions": false
}
}
}
"#
.unindent(),
|settings| {
settings.editor.copilot = Some(OnOff::On);
settings.editor.show_copilot_suggestions = Some(true);
},
r#"
{
"copilot": "on",
"show_copilot_suggestions": true,
"languages": {
"JSON": {
"copilot": "off"
"show_copilot_suggestions": false
}
}
}
@ -1187,13 +1164,13 @@ mod tests {
}
#[test]
fn test_update_langauge_copilot() {
fn test_update_language_copilot() {
assert_new_settings(
r#"
{
"languages": {
"JSON": {
"copilot": "off"
"show_copilot_suggestions": false
}
}
}
@ -1203,7 +1180,7 @@ mod tests {
settings.languages.insert(
"Rust".into(),
EditorSettings {
copilot: Some(OnOff::On),
show_copilot_suggestions: Some(true),
..Default::default()
},
);
@ -1212,10 +1189,10 @@ mod tests {
{
"languages": {
"Rust": {
"copilot": "on"
"show_copilot_suggestions": true
},
"JSON": {
"copilot": "off"
"show_copilot_suggestions": false
}
}
}

View file

@ -154,6 +154,12 @@ impl<K> TreeSet<K>
where
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) {
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,
anchors: impl IntoIterator<Item = &'a Anchor>,
anchors: impl IntoIterator<Item = Anchor>,
) -> impl 'static + Future<Output = Result<()>> {
let mut futures = Vec::new();
for anchor in anchors {
if !self.version.observed(anchor.timestamp)
&& *anchor != Anchor::MAX
&& *anchor != Anchor::MIN
&& anchor != Anchor::MAX
&& anchor != Anchor::MIN
{
let (tx, rx) = oneshot::channel();
self.edit_id_resolvers

View file

@ -785,6 +785,10 @@ impl Pane {
) -> Option<Task<Result<()>>> {
let pane_handle = workspace.active_pane().clone();
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 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 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]
async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.82.0"
version = "0.82.9"
publish = false
[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),
highlightedLineBackground: background(layer, "on"),
// Inline autocomplete suggestions, Co-pilot suggestions, etc.
suggestion: {
color: syntax.predictive.color,
},
suggestion: syntax.predictive,
codeActions: {
indicator: {
color: foreground(layer, "variant"),

View file

@ -1,6 +1,7 @@
import deepmerge from "deepmerge"
import { FontWeight, fontWeights } from "../../common"
import { ColorScheme } from "./colorScheme"
import chroma from "chroma-js"
export interface SyntaxHighlightStyle {
color: string
@ -128,6 +129,8 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
[key: string]: Omit<SyntaxHighlightStyle, "color">
} = {}
const light = colorScheme.isLight
// then spread the default to each style
for (const key of Object.keys({} as 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 = {
primary: colorScheme.ramps.neutral(1).hex(),
comment: colorScheme.ramps.neutral(0.71).hex(),
punctuation: colorScheme.ramps.neutral(0.86).hex(),
predictive: colorScheme.ramps.neutral(0.57).hex(),
predictive: predictive,
emphasis: colorScheme.ramps.blue(0.5).hex(),
string: colorScheme.ramps.orange(0.5).hex(),
function: colorScheme.ramps.yellow(0.5).hex(),
@ -169,6 +181,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
},
predictive: {
color: color.predictive,
italic: true,
},
emphasis: {
color: color.emphasis,