From dea64d3373965ebc4f241bdee1edb703576c6505 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Mon, 4 Aug 2025 05:49:51 -0700 Subject: [PATCH 01/46] Add icon for KDL files (#35377) 1753920601 Release Notes: - Added icon for KDL (`.kdl`) files --- assets/icons/file_icons/kdl.svg | 1 + crates/theme/src/icon_theme.rs | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 assets/icons/file_icons/kdl.svg diff --git a/assets/icons/file_icons/kdl.svg b/assets/icons/file_icons/kdl.svg new file mode 100644 index 0000000000..92d9f28428 --- /dev/null +++ b/assets/icons/file_icons/kdl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index baa928d722..10fd1e002d 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -152,6 +152,7 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ ("javascript", &["cjs", "js", "mjs"]), ("json", &["json"]), ("julia", &["jl"]), + ("kdl", &["kdl"]), ("kotlin", &["kt"]), ("lock", &["lock"]), ("log", &["log"]), @@ -315,6 +316,7 @@ const FILE_ICONS: &[(&str, &str)] = &[ ("javascript", "icons/file_icons/javascript.svg"), ("json", "icons/file_icons/code.svg"), ("julia", "icons/file_icons/julia.svg"), + ("kdl", "icons/file_icons/kdl.svg"), ("kotlin", "icons/file_icons/kotlin.svg"), ("lock", "icons/file_icons/lock.svg"), ("log", "icons/file_icons/info.svg"), From f17943e4a3ce724c13d724c33c87f4b62868857e Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 4 Aug 2025 15:49:41 +0200 Subject: [PATCH 02/46] Update to new agent schema (#35578) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga --- Cargo.lock | 23 +- Cargo.toml | 2 +- crates/acp_thread/Cargo.toml | 2 - crates/acp_thread/src/acp_thread.rs | 570 +++++++----------- crates/acp_thread/src/connection.rs | 22 +- crates/agent_servers/Cargo.toml | 3 +- crates/agent_servers/src/acp.rs | 34 ++ .../src/acp/v0.rs} | 98 ++- crates/agent_servers/src/acp/v1.rs | 254 ++++++++ crates/agent_servers/src/agent_servers.rs | 5 +- crates/agent_servers/src/claude.rs | 17 +- crates/agent_servers/src/codex.rs | 319 ---------- crates/agent_servers/src/e2e_tests.rs | 3 - crates/agent_servers/src/gemini.rs | 157 +---- crates/agent_servers/src/mcp_server.rs | 207 ------- crates/agent_servers/src/settings.rs | 11 +- crates/agent_ui/src/acp/thread_view.rs | 87 ++- crates/agent_ui/src/agent_panel.rs | 33 - crates/agent_ui/src/agent_ui.rs | 2 - crates/context_server/src/client.rs | 14 +- crates/context_server/src/context_server.rs | 27 +- crates/context_server/src/listener.rs | 10 +- crates/context_server/src/protocol.rs | 9 +- 23 files changed, 741 insertions(+), 1168 deletions(-) create mode 100644 crates/agent_servers/src/acp.rs rename crates/{acp_thread/src/old_acp_support.rs => agent_servers/src/acp/v0.rs} (84%) create mode 100644 crates/agent_servers/src/acp/v1.rs delete mode 100644 crates/agent_servers/src/codex.rs delete mode 100644 crates/agent_servers/src/mcp_server.rs diff --git a/Cargo.lock b/Cargo.lock index 45684b8920..56210557d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,10 +7,8 @@ name = "acp_thread" version = "0.1.0" dependencies = [ "agent-client-protocol", - "agentic-coding-protocol", "anyhow", "assistant_tool", - "async-pipe", "buffer_diff", "editor", "env_logger 0.11.8", @@ -139,10 +137,14 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.11" +version = "0.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ec54650c1fc2d63498bab47eeeaa9eddc7d239d53f615b797a0e84f7ccc87b" +checksum = "22c5180e40d31a9998ffa5f8eb067667f0870908a4aeed65a6a299e2d1d95443" dependencies = [ + "anyhow", + "futures 0.3.31", + "log", + "parking_lot", "schemars", "serde", "serde_json", @@ -177,6 +179,7 @@ dependencies = [ "smol", "strum 0.27.1", "tempfile", + "thiserror 2.0.12", "ui", "util", "uuid", @@ -9572,9 +9575,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -11288,9 +11291,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -11298,9 +11301,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", diff --git a/Cargo.toml b/Cargo.toml index 5b97596d0c..5d852f8842 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -421,7 +421,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.11" +agent-client-protocol = "0.0.17" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index cd7a5c3808..225597415c 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -17,7 +17,6 @@ test-support = ["gpui/test-support", "project/test-support"] [dependencies] agent-client-protocol.workspace = true -agentic-coding-protocol.workspace = true anyhow.workspace = true assistant_tool.workspace = true buffer_diff.workspace = true @@ -37,7 +36,6 @@ util.workspace = true workspace-hack.workspace = true [dev-dependencies] -async-pipe.workspace = true env_logger.workspace = true gpui = { workspace = true, "features" = ["test-support"] } indoc.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 0996dee723..079a207358 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1,7 +1,5 @@ mod connection; -mod old_acp_support; pub use connection::*; -pub use old_acp_support::*; use agent_client_protocol as acp; use anyhow::{Context as _, Result}; @@ -391,7 +389,7 @@ impl ToolCallContent { cx: &mut App, ) -> Self { match content { - acp::ToolCallContent::ContentBlock(content) => Self::ContentBlock { + acp::ToolCallContent::Content { content } => Self::ContentBlock { content: ContentBlock::new(content, &language_registry, cx), }, acp::ToolCallContent::Diff { diff } => Self::Diff { @@ -619,6 +617,7 @@ impl Error for LoadError {} impl AcpThread { pub fn new( + title: impl Into, connection: Rc, project: Entity, session_id: acp::SessionId, @@ -631,7 +630,7 @@ impl AcpThread { shared_buffers: Default::default(), entries: Default::default(), plan: Default::default(), - title: connection.name().into(), + title: title.into(), project, send_task: None, connection, @@ -708,14 +707,14 @@ impl AcpThread { cx: &mut Context, ) -> Result<()> { match update { - acp::SessionUpdate::UserMessage(content_block) => { - self.push_user_content_block(content_block, cx); + acp::SessionUpdate::UserMessageChunk { content } => { + self.push_user_content_block(content, cx); } - acp::SessionUpdate::AgentMessageChunk(content_block) => { - self.push_assistant_content_block(content_block, false, cx); + acp::SessionUpdate::AgentMessageChunk { content } => { + self.push_assistant_content_block(content, false, cx); } - acp::SessionUpdate::AgentThoughtChunk(content_block) => { - self.push_assistant_content_block(content_block, true, cx); + acp::SessionUpdate::AgentThoughtChunk { content } => { + self.push_assistant_content_block(content, true, cx); } acp::SessionUpdate::ToolCall(tool_call) => { self.upsert_tool_call(tool_call, cx); @@ -984,10 +983,6 @@ impl AcpThread { cx.notify(); } - pub fn authenticate(&self, cx: &mut App) -> impl use<> + Future> { - self.connection.authenticate(cx) - } - #[cfg(any(test, feature = "test-support"))] pub fn send_raw( &mut self, @@ -1029,7 +1024,7 @@ impl AcpThread { let result = this .update(cx, |this, cx| { this.connection.prompt( - acp::PromptArguments { + acp::PromptRequest { prompt: message, session_id: this.session_id.clone(), }, @@ -1239,21 +1234,15 @@ impl AcpThread { #[cfg(test)] mod tests { use super::*; - use agentic_coding_protocol as acp_old; use anyhow::anyhow; - use async_pipe::{PipeReader, PipeWriter}; - use futures::{ - channel::mpsc, - future::{LocalBoxFuture, try_join_all}, - select, - }; + use futures::{channel::mpsc, future::LocalBoxFuture, select}; use gpui::{AsyncApp, TestAppContext, WeakEntity}; use indoc::indoc; use project::FakeFs; use rand::Rng as _; use serde_json::json; use settings::SettingsStore; - use smol::{future::BoxedLocal, stream::StreamExt as _}; + use smol::stream::StreamExt as _; use std::{cell::RefCell, rc::Rc, time::Duration}; use util::path; @@ -1274,7 +1263,15 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let (thread, _fake_server) = fake_acp_thread(project, cx); + let connection = Rc::new(FakeAgentConnection::new()); + let thread = cx + .spawn(async move |mut cx| { + connection + .new_thread(project, Path::new(path!("/test")), &mut cx) + .await + }) + .await + .unwrap(); // Test creating a new user message thread.update(cx, |thread, cx| { @@ -1354,34 +1351,40 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let (thread, fake_server) = fake_acp_thread(project, cx); + let connection = Rc::new(FakeAgentConnection::new().on_user_message( + |_, thread, mut cx| { + async move { + thread.update(&mut cx, |thread, cx| { + thread + .handle_session_update( + acp::SessionUpdate::AgentThoughtChunk { + content: "Thinking ".into(), + }, + cx, + ) + .unwrap(); + thread + .handle_session_update( + acp::SessionUpdate::AgentThoughtChunk { + content: "hard!".into(), + }, + cx, + ) + .unwrap(); + }) + } + .boxed_local() + }, + )); - fake_server.update(cx, |fake_server, _| { - fake_server.on_user_message(move |_, server, mut cx| async move { - server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::StreamAssistantMessageChunkParams { - chunk: acp_old::AssistantMessageChunk::Thought { - thought: "Thinking ".into(), - }, - }) - })? + let thread = cx + .spawn(async move |mut cx| { + connection + .new_thread(project, Path::new(path!("/test")), &mut cx) .await - .unwrap(); - server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::StreamAssistantMessageChunkParams { - chunk: acp_old::AssistantMessageChunk::Thought { - thought: "hard!".into(), - }, - }) - })? - .await - .unwrap(); - - Ok(()) }) - }); + .await + .unwrap(); thread .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) @@ -1414,7 +1417,38 @@ mod tests { fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\n"})) .await; let project = Project::test(fs.clone(), [], cx).await; - let (thread, fake_server) = fake_acp_thread(project.clone(), cx); + let (read_file_tx, read_file_rx) = oneshot::channel::<()>(); + let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx))); + let connection = Rc::new(FakeAgentConnection::new().on_user_message( + move |_, thread, mut cx| { + let read_file_tx = read_file_tx.clone(); + async move { + let content = thread + .update(&mut cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(content, "one\ntwo\nthree\n"); + read_file_tx.take().unwrap().send(()).unwrap(); + thread + .update(&mut cx, |thread, cx| { + thread.write_text_file( + path!("/tmp/foo").into(), + "one\ntwo\nthree\nfour\nfive\n".to_string(), + cx, + ) + }) + .unwrap() + .await + .unwrap(); + Ok(()) + } + .boxed_local() + }, + )); + let (worktree, pathbuf) = project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/tmp/foo"), true, cx) @@ -1428,38 +1462,10 @@ mod tests { .await .unwrap(); - let (read_file_tx, read_file_rx) = oneshot::channel::<()>(); - let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx))); - - fake_server.update(cx, |fake_server, _| { - fake_server.on_user_message(move |_, server, mut cx| { - let read_file_tx = read_file_tx.clone(); - async move { - let content = server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::ReadTextFileParams { - path: path!("/tmp/foo").into(), - line: None, - limit: None, - }) - })? - .await - .unwrap(); - assert_eq!(content.content, "one\ntwo\nthree\n"); - read_file_tx.take().unwrap().send(()).unwrap(); - server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::WriteTextFileParams { - path: path!("/tmp/foo").into(), - content: "one\ntwo\nthree\nfour\nfive\n".to_string(), - }) - })? - .await - .unwrap(); - Ok(()) - } - }) - }); + let thread = cx + .spawn(|mut cx| connection.new_thread(project, Path::new(path!("/tmp")), &mut cx)) + .await + .unwrap(); let request = thread.update(cx, |thread, cx| { thread.send_raw("Extend the count in /tmp/foo", cx) @@ -1486,36 +1492,44 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let (thread, fake_server) = fake_acp_thread(project, cx); + let id = acp::ToolCallId("test".into()); - let (end_turn_tx, end_turn_rx) = oneshot::channel::<()>(); - - let tool_call_id = Rc::new(RefCell::new(None)); - let end_turn_rx = Rc::new(RefCell::new(Some(end_turn_rx))); - fake_server.update(cx, |fake_server, _| { - let tool_call_id = tool_call_id.clone(); - fake_server.on_user_message(move |_, server, mut cx| { - let end_turn_rx = end_turn_rx.clone(); - let tool_call_id = tool_call_id.clone(); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + let id = id.clone(); + move |_, thread, mut cx| { + let id = id.clone(); async move { - let tool_call_result = server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::PushToolCallParams { - label: "Fetch".to_string(), - icon: acp_old::Icon::Globe, - content: None, - locations: vec![], - }) - })? - .await + thread + .update(&mut cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::ToolCall(acp::ToolCall { + id: id.clone(), + label: "Label".into(), + kind: acp::ToolKind::Fetch, + status: acp::ToolCallStatus::InProgress, + content: vec![], + locations: vec![], + raw_input: None, + }), + cx, + ) + }) + .unwrap() .unwrap(); - *tool_call_id.clone().borrow_mut() = Some(tool_call_result.id); - end_turn_rx.take().unwrap().await.ok(); - Ok(()) } + .boxed_local() + } + })); + + let thread = cx + .spawn(async move |mut cx| { + connection + .new_thread(project, Path::new(path!("/test")), &mut cx) + .await }) - }); + .await + .unwrap(); let request = thread.update(cx, |thread, cx| { thread.send_raw("Fetch https://example.com", cx) @@ -1536,8 +1550,6 @@ mod tests { )); }); - cx.run_until_parked(); - thread.update(cx, |thread, cx| thread.cancel(cx)).await; thread.read_with(cx, |thread, _| { @@ -1550,19 +1562,22 @@ mod tests { )); }); - fake_server - .update(cx, |fake_server, _| { - fake_server.send_to_zed(acp_old::UpdateToolCallParams { - tool_call_id: tool_call_id.borrow().unwrap(), - status: acp_old::ToolCallStatus::Finished, - content: None, - }) + thread + .update(cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate { + id, + fields: acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::Completed), + ..Default::default() + }, + }), + cx, + ) }) - .await .unwrap(); - drop(end_turn_tx); - assert!(request.await.unwrap_err().to_string().contains("canceled")); + request.await.unwrap(); thread.read_with(cx, |thread, _| { assert!(matches!( @@ -1585,23 +1600,37 @@ mod tests { fs.insert_tree(path!("/test"), json!({})).await; let project = Project::test(fs, [path!("/test").as_ref()], cx).await; - let connection = Rc::new(StubAgentConnection::new(vec![ - acp::SessionUpdate::ToolCall(acp::ToolCall { - id: acp::ToolCallId("test".into()), - label: "Label".into(), - kind: acp::ToolKind::Edit, - status: acp::ToolCallStatus::Completed, - content: vec![acp::ToolCallContent::Diff { - diff: acp::Diff { - path: "/test/test.txt".into(), - old_text: None, - new_text: "foo".into(), - }, - }], - locations: vec![], - raw_input: None, - }), - ])); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + move |_, thread, mut cx| { + async move { + thread + .update(&mut cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::ToolCall(acp::ToolCall { + id: acp::ToolCallId("test".into()), + label: "Label".into(), + kind: acp::ToolKind::Edit, + status: acp::ToolCallStatus::Completed, + content: vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: "/test/test.txt".into(), + old_text: None, + new_text: "foo".into(), + }, + }], + locations: vec![], + raw_input: None, + }), + cx, + ) + }) + .unwrap() + .unwrap(); + Ok(()) + } + .boxed_local() + } + })); let thread = connection .new_thread(project, Path::new(path!("/test")), &mut cx.to_async()) @@ -1642,25 +1671,53 @@ mod tests { } #[derive(Clone, Default)] - struct StubAgentConnection { + struct FakeAgentConnection { + auth_methods: Vec, sessions: Arc>>>, - permission_requests: HashMap>, - updates: Vec, + on_user_message: Option< + Rc< + dyn Fn( + acp::PromptRequest, + WeakEntity, + AsyncApp, + ) -> LocalBoxFuture<'static, Result<()>> + + 'static, + >, + >, } - impl StubAgentConnection { - fn new(updates: Vec) -> Self { + impl FakeAgentConnection { + fn new() -> Self { Self { - updates, - permission_requests: HashMap::default(), + auth_methods: Vec::new(), + on_user_message: None, sessions: Arc::default(), } } + + #[expect(unused)] + fn with_auth_methods(mut self, auth_methods: Vec) -> Self { + self.auth_methods = auth_methods; + self + } + + fn on_user_message( + mut self, + handler: impl Fn( + acp::PromptRequest, + WeakEntity, + AsyncApp, + ) -> LocalBoxFuture<'static, Result<()>> + + 'static, + ) -> Self { + self.on_user_message.replace(Rc::new(handler)); + self + } } - impl AgentConnection for StubAgentConnection { - fn name(&self) -> &'static str { - "StubAgentConnection" + impl AgentConnection for FakeAgentConnection { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &self.auth_methods } fn new_thread( @@ -1678,222 +1735,43 @@ mod tests { .into(), ); let thread = cx - .new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx)) + .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)) .unwrap(); self.sessions.lock().insert(session_id, thread.downgrade()); Task::ready(Ok(thread)) } - fn authenticate(&self, _cx: &mut App) -> Task> { - unimplemented!() + fn authenticate(&self, method: acp::AuthMethodId, _cx: &mut App) -> Task> { + if self.auth_methods().iter().any(|m| m.id == method) { + Task::ready(Ok(())) + } else { + Task::ready(Err(anyhow!("Invalid Auth Method"))) + } } - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { let sessions = self.sessions.lock(); let thread = sessions.get(¶ms.session_id).unwrap(); - let mut tasks = vec![]; - for update in &self.updates { + if let Some(handler) = &self.on_user_message { + let handler = handler.clone(); let thread = thread.clone(); - let update = update.clone(); - let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update - && let Some(options) = self.permission_requests.get(&tool_call.id) - { - Some((tool_call.clone(), options.clone())) - } else { - None - }; - let task = cx.spawn(async move |cx| { - if let Some((tool_call, options)) = permission_request { - let permission = thread.update(cx, |thread, cx| { - thread.request_tool_call_permission( - tool_call.clone(), - options.clone(), - cx, - ) - })?; - permission.await?; - } - thread.update(cx, |thread, cx| { - thread.handle_session_update(update.clone(), cx).unwrap(); - })?; - anyhow::Ok(()) - }); - tasks.push(task); - } - cx.spawn(async move |_| { - try_join_all(tasks).await?; - Ok(()) - }) - } - - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { - unimplemented!() - } - } - - pub fn fake_acp_thread( - project: Entity, - cx: &mut TestAppContext, - ) -> (Entity, Entity) { - let (stdin_tx, stdin_rx) = async_pipe::pipe(); - let (stdout_tx, stdout_rx) = async_pipe::pipe(); - - let thread = cx.new(|cx| { - let foreground_executor = cx.foreground_executor().clone(); - let thread_rc = Rc::new(RefCell::new(cx.entity().downgrade())); - - let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( - OldAcpClientDelegate::new(thread_rc.clone(), cx.to_async()), - stdin_tx, - stdout_rx, - move |fut| { - foreground_executor.spawn(fut).detach(); - }, - ); - - let io_task = cx.background_spawn({ - async move { - io_fut.await.log_err(); - Ok(()) - } - }); - let connection = OldAcpAgentConnection { - name: "test", - connection, - child_status: io_task, - current_thread: thread_rc, - }; - - AcpThread::new( - Rc::new(connection), - project, - acp::SessionId("test".into()), - cx, - ) - }); - let agent = cx.update(|cx| cx.new(|cx| FakeAcpServer::new(stdin_rx, stdout_tx, cx))); - (thread, agent) - } - - pub struct FakeAcpServer { - connection: acp_old::ClientConnection, - - _io_task: Task<()>, - on_user_message: Option< - Rc< - dyn Fn( - acp_old::SendUserMessageParams, - Entity, - AsyncApp, - ) -> LocalBoxFuture<'static, Result<(), acp_old::Error>>, - >, - >, - } - - #[derive(Clone)] - struct FakeAgent { - server: Entity, - cx: AsyncApp, - cancel_tx: Rc>>>, - } - - impl acp_old::Agent for FakeAgent { - async fn initialize( - &self, - params: acp_old::InitializeParams, - ) -> Result { - Ok(acp_old::InitializeResponse { - protocol_version: params.protocol_version, - is_authenticated: true, - }) - } - - async fn authenticate(&self) -> Result<(), acp_old::Error> { - Ok(()) - } - - async fn cancel_send_message(&self) -> Result<(), acp_old::Error> { - if let Some(cancel_tx) = self.cancel_tx.take() { - cancel_tx.send(()).log_err(); - } - Ok(()) - } - - async fn send_user_message( - &self, - request: acp_old::SendUserMessageParams, - ) -> Result<(), acp_old::Error> { - let (cancel_tx, cancel_rx) = oneshot::channel(); - self.cancel_tx.replace(Some(cancel_tx)); - - let mut cx = self.cx.clone(); - let handler = self - .server - .update(&mut cx, |server, _| server.on_user_message.clone()) - .ok() - .flatten(); - if let Some(handler) = handler { - select! { - _ = cancel_rx.fuse() => Err(anyhow::anyhow!("Message sending canceled").into()), - _ = handler(request, self.server.clone(), self.cx.clone()).fuse() => Ok(()), - } + cx.spawn(async move |cx| handler(params, thread, cx.clone()).await) } else { - Err(anyhow::anyhow!("No handler for on_user_message").into()) - } - } - } - - impl FakeAcpServer { - fn new(stdin: PipeReader, stdout: PipeWriter, cx: &Context) -> Self { - let agent = FakeAgent { - server: cx.entity(), - cx: cx.to_async(), - cancel_tx: Default::default(), - }; - let foreground_executor = cx.foreground_executor().clone(); - - let (connection, io_fut) = acp_old::ClientConnection::connect_to_client( - agent.clone(), - stdout, - stdin, - move |fut| { - foreground_executor.spawn(fut).detach(); - }, - ); - FakeAcpServer { - connection: connection, - on_user_message: None, - _io_task: cx.background_spawn(async move { - io_fut.await.log_err(); - }), + Task::ready(Ok(())) } } - fn on_user_message( - &mut self, - handler: impl for<'a> Fn( - acp_old::SendUserMessageParams, - Entity, - AsyncApp, - ) -> F - + 'static, - ) where - F: Future> + 'static, - { - self.on_user_message - .replace(Rc::new(move |request, server, cx| { - handler(request, server, cx).boxed_local() - })); - } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + let sessions = self.sessions.lock(); + let thread = sessions.get(&session_id).unwrap().clone(); - fn send_to_zed( - &self, - message: T, - ) -> BoxedLocal> { - self.connection - .request(message) - .map(|f| f.map_err(|err| anyhow!(err))) - .boxed_local() + cx.spawn(async move |cx| { + thread + .update(cx, |thread, cx| thread.cancel(cx)) + .unwrap() + .await + }) + .detach(); } } } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 5b25b71863..929500a67b 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -1,6 +1,6 @@ -use std::{path::Path, rc::Rc}; +use std::{error::Error, fmt, path::Path, rc::Rc}; -use agent_client_protocol as acp; +use agent_client_protocol::{self as acp}; use anyhow::Result; use gpui::{AsyncApp, Entity, Task}; use project::Project; @@ -9,8 +9,6 @@ use ui::App; use crate::AcpThread; pub trait AgentConnection { - fn name(&self) -> &'static str; - fn new_thread( self: Rc, project: Entity, @@ -18,9 +16,21 @@ pub trait AgentConnection { cx: &mut AsyncApp, ) -> Task>>; - fn authenticate(&self, cx: &mut App) -> Task>; + fn auth_methods(&self) -> &[acp::AuthMethod]; - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task>; + fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task>; + + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task>; fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); } + +#[derive(Debug)] +pub struct AuthRequired; + +impl Error for AuthRequired {} +impl fmt::Display for AuthRequired { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "AuthRequired") + } +} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index dcffb05bc0..81c97c8aa6 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -25,6 +25,7 @@ collections.workspace = true context_server.workspace = true futures.workspace = true gpui.workspace = true +indoc.workspace = true itertools.workspace = true log.workspace = true paths.workspace = true @@ -37,11 +38,11 @@ settings.workspace = true smol.workspace = true strum.workspace = true tempfile.workspace = true +thiserror.workspace = true ui.workspace = true util.workspace = true uuid.workspace = true watch.workspace = true -indoc.workspace = true which.workspace = true workspace-hack.workspace = true diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs new file mode 100644 index 0000000000..00e3e3df50 --- /dev/null +++ b/crates/agent_servers/src/acp.rs @@ -0,0 +1,34 @@ +use std::{path::Path, rc::Rc}; + +use crate::AgentServerCommand; +use acp_thread::AgentConnection; +use anyhow::Result; +use gpui::AsyncApp; +use thiserror::Error; + +mod v0; +mod v1; + +#[derive(Debug, Error)] +#[error("Unsupported version")] +pub struct UnsupportedVersion; + +pub async fn connect( + server_name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, +) -> Result> { + let conn = v1::AcpConnection::stdio(server_name, command.clone(), &root_dir, cx).await; + + match conn { + Ok(conn) => Ok(Rc::new(conn) as _), + Err(err) if err.is::() => { + // Consider re-using initialize response and subprocess when adding another version here + let conn: Rc = + Rc::new(v0::AcpConnection::stdio(server_name, command, &root_dir, cx).await?); + Ok(conn) + } + Err(err) => Err(err), + } +} diff --git a/crates/acp_thread/src/old_acp_support.rs b/crates/agent_servers/src/acp/v0.rs similarity index 84% rename from crates/acp_thread/src/old_acp_support.rs rename to crates/agent_servers/src/acp/v0.rs index 571023239f..6839ff2462 100644 --- a/crates/acp_thread/src/old_acp_support.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -1,18 +1,19 @@ // Translates old acp agents into the new schema use agent_client_protocol as acp; use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use project::Project; -use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc}; +use std::{cell::RefCell, path::Path, rc::Rc}; use ui::App; use util::ResultExt as _; -use crate::{AcpThread, AgentConnection}; +use crate::AgentServerCommand; +use acp_thread::{AcpThread, AgentConnection, AuthRequired}; #[derive(Clone)] -pub struct OldAcpClientDelegate { +struct OldAcpClientDelegate { thread: Rc>>, cx: AsyncApp, next_tool_call_id: Rc>, @@ -20,7 +21,7 @@ pub struct OldAcpClientDelegate { } impl OldAcpClientDelegate { - pub fn new(thread: Rc>>, cx: AsyncApp) -> Self { + fn new(thread: Rc>>, cx: AsyncApp) -> Self { Self { thread, cx, @@ -351,28 +352,71 @@ fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatu } } -#[derive(Debug)] -pub struct Unauthenticated; - -impl Error for Unauthenticated {} -impl fmt::Display for Unauthenticated { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Unauthenticated") - } -} - -pub struct OldAcpAgentConnection { +pub struct AcpConnection { pub name: &'static str, pub connection: acp_old::AgentConnection, - pub child_status: Task>, + pub _child_status: Task>, pub current_thread: Rc>>, } -impl AgentConnection for OldAcpAgentConnection { - fn name(&self) -> &'static str { - self.name - } +impl AcpConnection { + pub fn stdio( + name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Task> { + let root_dir = root_dir.to_path_buf(); + cx.spawn(async move |cx| { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .kill_on_drop(true) + .spawn()?; + + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + + let foreground_executor = cx.foreground_executor().clone(); + + let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); + + let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( + OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()), + stdin, + stdout, + move |fut| foreground_executor.spawn(fut).detach(), + ); + + let io_task = cx.background_spawn(async move { + io_fut.await.log_err(); + }); + + let child_status = cx.background_spawn(async move { + let result = match child.status().await { + Err(e) => Err(anyhow!(e)), + Ok(result) if result.success() => Ok(()), + Ok(result) => Err(anyhow!(result)), + }; + drop(io_task); + result + }); + + Ok(Self { + name, + connection, + _child_status: child_status, + current_thread: thread_rc, + }) + }) + } +} + +impl AgentConnection for AcpConnection { fn new_thread( self: Rc, project: Entity, @@ -391,13 +435,13 @@ impl AgentConnection for OldAcpAgentConnection { let result = acp_old::InitializeParams::response_from_any(result)?; if !result.is_authenticated { - anyhow::bail!(Unauthenticated) + anyhow::bail!(AuthRequired) } cx.update(|cx| { let thread = cx.new(|cx| { let session_id = acp::SessionId("acp-old-no-id".into()); - AcpThread::new(self.clone(), project, session_id, cx) + AcpThread::new(self.name, self.clone(), project, session_id, cx) }); current_thread.replace(thread.downgrade()); thread @@ -405,7 +449,11 @@ impl AgentConnection for OldAcpAgentConnection { }) } - fn authenticate(&self, cx: &mut App) -> Task> { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task> { let task = self .connection .request_any(acp_old::AuthenticateParams.into_any()); @@ -415,7 +463,7 @@ impl AgentConnection for OldAcpAgentConnection { }) } - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { let chunks = params .prompt .into_iter() diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs new file mode 100644 index 0000000000..9e2193ce18 --- /dev/null +++ b/crates/agent_servers/src/acp/v1.rs @@ -0,0 +1,254 @@ +use agent_client_protocol::{self as acp, Agent as _}; +use collections::HashMap; +use futures::channel::oneshot; +use project::Project; +use std::cell::RefCell; +use std::path::Path; +use std::rc::Rc; + +use anyhow::{Context as _, Result}; +use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; + +use crate::{AgentServerCommand, acp::UnsupportedVersion}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired}; + +pub struct AcpConnection { + server_name: &'static str, + connection: Rc, + sessions: Rc>>, + auth_methods: Vec, + _io_task: Task>, + _child: smol::process::Child, +} + +pub struct AcpSession { + thread: WeakEntity, +} + +const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; + +impl AcpConnection { + pub async fn stdio( + server_name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Result { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter().map(|arg| arg.as_str())) + .envs(command.env.iter().flatten()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .kill_on_drop(true) + .spawn()?; + + let stdout = child.stdout.take().expect("Failed to take stdout"); + let stdin = child.stdin.take().expect("Failed to take stdin"); + + let sessions = Rc::new(RefCell::new(HashMap::default())); + + let client = ClientDelegate { + sessions: sessions.clone(), + cx: cx.clone(), + }; + let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { + let foreground_executor = cx.foreground_executor().clone(); + move |fut| { + foreground_executor.spawn(fut).detach(); + } + }); + + let io_task = cx.background_spawn(io_task); + + let response = connection + .initialize(acp::InitializeRequest { + protocol_version: acp::VERSION, + client_capabilities: acp::ClientCapabilities { + fs: acp::FileSystemCapability { + read_text_file: true, + write_text_file: true, + }, + }, + }) + .await?; + + if response.protocol_version < MINIMUM_SUPPORTED_VERSION { + return Err(UnsupportedVersion.into()); + } + + Ok(Self { + auth_methods: response.auth_methods, + connection: connection.into(), + server_name, + sessions, + _child: child, + _io_task: io_task, + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut AsyncApp, + ) -> Task>> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let cwd = cwd.to_path_buf(); + cx.spawn(async move |cx| { + let response = conn + .new_session(acp::NewSessionRequest { + mcp_servers: vec![], + cwd, + }) + .await?; + + let Some(session_id) = response.session_id else { + anyhow::bail!(AuthRequired); + }; + + let thread = cx.new(|cx| { + AcpThread::new( + self.server_name, + self.clone(), + project, + session_id.clone(), + cx, + ) + })?; + + let session = AcpSession { + thread: thread.downgrade(), + }; + sessions.borrow_mut().insert(session_id, session); + + Ok(thread) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &self.auth_methods + } + + fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> { + let conn = self.connection.clone(); + cx.foreground_executor().spawn(async move { + let result = conn + .authenticate(acp::AuthenticateRequest { + method_id: method_id.clone(), + }) + .await?; + + Ok(result) + }) + } + + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { + let conn = self.connection.clone(); + cx.foreground_executor() + .spawn(async move { Ok(conn.prompt(params).await?) }) + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + let conn = self.connection.clone(); + let params = acp::CancelledNotification { + session_id: session_id.clone(), + }; + cx.foreground_executor() + .spawn(async move { conn.cancelled(params).await }) + .detach(); + } +} + +struct ClientDelegate { + sessions: Rc>>, + cx: AsyncApp, +} + +impl acp::Client for ClientDelegate { + async fn request_permission( + &self, + arguments: acp::RequestPermissionRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let rx = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.request_tool_call_permission(arguments.tool_call, arguments.options, cx) + })?; + + let result = rx.await; + + let outcome = match result { + Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, + }; + + Ok(acp::RequestPermissionResponse { outcome }) + } + + async fn write_text_file( + &self, + arguments: acp::WriteTextFileRequest, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.write_text_file(arguments.path, arguments.content, cx) + })?; + + task.await?; + + Ok(()) + } + + async fn read_text_file( + &self, + arguments: acp::ReadTextFileRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) + })?; + + let content = task.await?; + + Ok(acp::ReadTextFileResponse { content }) + } + + async fn session_notification( + &self, + notification: acp::SessionNotification, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let sessions = self.sessions.borrow(); + let session = sessions + .get(¬ification.session_id) + .context("Failed to get session")?; + + session.thread.update(cx, |thread, cx| { + thread.handle_session_update(notification.update, cx) + })??; + + Ok(()) + } +} diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 212bb74d8a..ec69290206 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,14 +1,12 @@ +mod acp; mod claude; -mod codex; mod gemini; -mod mcp_server; mod settings; #[cfg(test)] mod e2e_tests; pub use claude::*; -pub use codex::*; pub use gemini::*; pub use settings::*; @@ -38,7 +36,6 @@ pub trait AgentServer: Send { fn connect( &self, - // these will go away when old_acp is fully removed root_dir: &Path, project: &Entity, cx: &mut App, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 6565786204..9040b83085 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -70,10 +70,6 @@ struct ClaudeAgentConnection { } impl AgentConnection for ClaudeAgentConnection { - fn name(&self) -> &'static str { - ClaudeCode.name() - } - fn new_thread( self: Rc, project: Entity, @@ -168,8 +164,9 @@ impl AgentConnection for ClaudeAgentConnection { } }); - let thread = - cx.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))?; + let thread = cx.new(|cx| { + AcpThread::new("Claude Code", self.clone(), project, session_id.clone(), cx) + })?; thread_tx.send(thread.downgrade())?; @@ -186,11 +183,15 @@ impl AgentConnection for ClaudeAgentConnection { }) } - fn authenticate(&self, _cx: &mut App) -> Task> { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate(&self, _: acp::AuthMethodId, _cx: &mut App) -> Task> { Task::ready(Err(anyhow!("Authentication not supported"))) } - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(¶ms.session_id) else { return Task::ready(Err(anyhow!( diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs deleted file mode 100644 index 712c333221..0000000000 --- a/crates/agent_servers/src/codex.rs +++ /dev/null @@ -1,319 +0,0 @@ -use agent_client_protocol as acp; -use anyhow::anyhow; -use collections::HashMap; -use context_server::listener::McpServerTool; -use context_server::types::requests; -use context_server::{ContextServer, ContextServerCommand, ContextServerId}; -use futures::channel::{mpsc, oneshot}; -use project::Project; -use settings::SettingsStore; -use smol::stream::StreamExt as _; -use std::cell::RefCell; -use std::rc::Rc; -use std::{path::Path, sync::Arc}; -use util::ResultExt; - -use anyhow::{Context, Result}; -use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; - -use crate::mcp_server::ZedMcpServer; -use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings, mcp_server}; -use acp_thread::{AcpThread, AgentConnection}; - -#[derive(Clone)] -pub struct Codex; - -impl AgentServer for Codex { - fn name(&self) -> &'static str { - "Codex" - } - - fn empty_state_headline(&self) -> &'static str { - "Welcome to Codex" - } - - fn empty_state_message(&self) -> &'static str { - "What can I help with?" - } - - fn logo(&self) -> ui::IconName { - ui::IconName::AiOpenAi - } - - fn connect( - &self, - _root_dir: &Path, - project: &Entity, - cx: &mut App, - ) -> Task>> { - let project = project.clone(); - let working_directory = project.read(cx).active_project_directory(cx); - cx.spawn(async move |cx| { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).codex.clone() - })?; - - let Some(command) = - AgentServerCommand::resolve("codex", &["mcp"], settings, &project, cx).await - else { - anyhow::bail!("Failed to find codex binary"); - }; - - let client: Arc = ContextServer::stdio( - ContextServerId("codex-mcp-server".into()), - ContextServerCommand { - path: command.path, - args: command.args, - env: command.env, - }, - working_directory, - ) - .into(); - ContextServer::start(client.clone(), cx).await?; - - let (notification_tx, mut notification_rx) = mpsc::unbounded(); - client - .client() - .context("Failed to subscribe")? - .on_notification(acp::SESSION_UPDATE_METHOD_NAME, { - move |notification, _cx| { - let notification_tx = notification_tx.clone(); - log::trace!( - "ACP Notification: {}", - serde_json::to_string_pretty(¬ification).unwrap() - ); - - if let Some(notification) = - serde_json::from_value::(notification) - .log_err() - { - notification_tx.unbounded_send(notification).ok(); - } - } - }); - - let sessions = Rc::new(RefCell::new(HashMap::default())); - - let notification_handler_task = cx.spawn({ - let sessions = sessions.clone(); - async move |cx| { - while let Some(notification) = notification_rx.next().await { - CodexConnection::handle_session_notification( - notification, - sessions.clone(), - cx, - ) - } - } - }); - - let connection = CodexConnection { - client, - sessions, - _notification_handler_task: notification_handler_task, - }; - Ok(Rc::new(connection) as _) - }) - } -} - -struct CodexConnection { - client: Arc, - sessions: Rc>>, - _notification_handler_task: Task<()>, -} - -struct CodexSession { - thread: WeakEntity, - cancel_tx: Option>, - _mcp_server: ZedMcpServer, -} - -impl AgentConnection for CodexConnection { - fn name(&self) -> &'static str { - "Codex" - } - - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut AsyncApp, - ) -> Task>> { - let client = self.client.client(); - let sessions = self.sessions.clone(); - let cwd = cwd.to_path_buf(); - cx.spawn(async move |cx| { - let client = client.context("MCP server is not initialized yet")?; - let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); - - let mcp_server = ZedMcpServer::new(thread_rx, cx).await?; - - let response = client - .request::(context_server::types::CallToolParams { - name: acp::NEW_SESSION_TOOL_NAME.into(), - arguments: Some(serde_json::to_value(acp::NewSessionArguments { - mcp_servers: [( - mcp_server::SERVER_NAME.to_string(), - mcp_server.server_config()?, - )] - .into(), - client_tools: acp::ClientTools { - request_permission: Some(acp::McpToolId { - mcp_server: mcp_server::SERVER_NAME.into(), - tool_name: mcp_server::RequestPermissionTool::NAME.into(), - }), - read_text_file: Some(acp::McpToolId { - mcp_server: mcp_server::SERVER_NAME.into(), - tool_name: mcp_server::ReadTextFileTool::NAME.into(), - }), - write_text_file: Some(acp::McpToolId { - mcp_server: mcp_server::SERVER_NAME.into(), - tool_name: mcp_server::WriteTextFileTool::NAME.into(), - }), - }, - cwd, - })?), - meta: None, - }) - .await?; - - if response.is_error.unwrap_or_default() { - return Err(anyhow!(response.text_contents())); - } - - let result = serde_json::from_value::( - response.structured_content.context("Empty response")?, - )?; - - let thread = - cx.new(|cx| AcpThread::new(self.clone(), project, result.session_id.clone(), cx))?; - - thread_tx.send(thread.downgrade())?; - - let session = CodexSession { - thread: thread.downgrade(), - cancel_tx: None, - _mcp_server: mcp_server, - }; - sessions.borrow_mut().insert(result.session_id, session); - - Ok(thread) - }) - } - - fn authenticate(&self, _cx: &mut App) -> Task> { - Task::ready(Err(anyhow!("Authentication not supported"))) - } - - fn prompt( - &self, - params: agent_client_protocol::PromptArguments, - cx: &mut App, - ) -> Task> { - let client = self.client.client(); - let sessions = self.sessions.clone(); - - cx.foreground_executor().spawn(async move { - let client = client.context("MCP server is not initialized yet")?; - - let (new_cancel_tx, cancel_rx) = oneshot::channel(); - { - let mut sessions = sessions.borrow_mut(); - let session = sessions - .get_mut(¶ms.session_id) - .context("Session not found")?; - session.cancel_tx.replace(new_cancel_tx); - } - - let result = client - .request_with::( - context_server::types::CallToolParams { - name: acp::PROMPT_TOOL_NAME.into(), - arguments: Some(serde_json::to_value(params)?), - meta: None, - }, - Some(cancel_rx), - None, - ) - .await; - - if let Err(err) = &result - && err.is::() - { - return Ok(()); - } - - let response = result?; - - if response.is_error.unwrap_or_default() { - return Err(anyhow!(response.text_contents())); - } - - Ok(()) - }) - } - - fn cancel(&self, session_id: &agent_client_protocol::SessionId, _cx: &mut App) { - let mut sessions = self.sessions.borrow_mut(); - - if let Some(cancel_tx) = sessions - .get_mut(session_id) - .and_then(|session| session.cancel_tx.take()) - { - cancel_tx.send(()).ok(); - } - } -} - -impl CodexConnection { - pub fn handle_session_notification( - notification: acp::SessionNotification, - threads: Rc>>, - cx: &mut AsyncApp, - ) { - let threads = threads.borrow(); - let Some(thread) = threads - .get(¬ification.session_id) - .and_then(|session| session.thread.upgrade()) - else { - log::error!( - "Thread not found for session ID: {}", - notification.session_id - ); - return; - }; - - thread - .update(cx, |thread, cx| { - thread.handle_session_update(notification.update, cx) - }) - .log_err(); - } -} - -impl Drop for CodexConnection { - fn drop(&mut self) { - self.client.stop().log_err(); - } -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::AgentServerCommand; - use std::path::Path; - - crate::common_e2e_tests!(Codex, allow_option_id = "approve"); - - pub fn local_command() -> AgentServerCommand { - let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../../codex/codex-rs/target/debug/codex"); - - AgentServerCommand { - path: cli_path, - args: vec![], - env: None, - } - } -} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index e9c72eabc9..16bf1e6b47 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -375,9 +375,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { gemini: Some(AgentServerSettings { command: crate::gemini::tests::local_command(), }), - codex: Some(AgentServerSettings { - command: crate::codex::tests::local_command(), - }), }, cx, ); diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index a97ff3f462..1119a8b4ee 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -1,14 +1,10 @@ -use anyhow::anyhow; -use std::cell::RefCell; use std::path::Path; use std::rc::Rc; -use util::ResultExt as _; -use crate::{AgentServer, AgentServerCommand, AgentServerVersion}; -use acp_thread::{AgentConnection, LoadError, OldAcpAgentConnection, OldAcpClientDelegate}; -use agentic_coding_protocol as acp_old; -use anyhow::{Context as _, Result}; -use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; +use crate::{AgentServer, AgentServerCommand}; +use acp_thread::AgentConnection; +use anyhow::Result; +use gpui::{Entity, Task}; use project::Project; use settings::SettingsStore; use ui::App; @@ -43,146 +39,25 @@ impl AgentServer for Gemini { project: &Entity, cx: &mut App, ) -> Task>> { - let root_dir = root_dir.to_path_buf(); let project = project.clone(); - let this = self.clone(); - let name = self.name(); - + let root_dir = root_dir.to_path_buf(); + let server_name = self.name(); cx.spawn(async move |cx| { - let command = this.command(&project, cx).await?; + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).gemini.clone() + })?; - let mut child = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .current_dir(root_dir) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::inherit()) - .kill_on_drop(true) - .spawn()?; + let Some(command) = + AgentServerCommand::resolve("gemini", &[ACP_ARG], settings, &project, cx).await + else { + anyhow::bail!("Failed to find gemini binary"); + }; - let stdin = child.stdin.take().unwrap(); - let stdout = child.stdout.take().unwrap(); - - let foreground_executor = cx.foreground_executor().clone(); - - let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); - - let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( - OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()), - stdin, - stdout, - move |fut| foreground_executor.spawn(fut).detach(), - ); - - let io_task = cx.background_spawn(async move { - io_fut.await.log_err(); - }); - - let child_status = cx.background_spawn(async move { - let result = match child.status().await { - Err(e) => Err(anyhow!(e)), - Ok(result) if result.success() => Ok(()), - Ok(result) => { - if let Some(AgentServerVersion::Unsupported { - error_message, - upgrade_message, - upgrade_command, - }) = this.version(&command).await.log_err() - { - Err(anyhow!(LoadError::Unsupported { - error_message, - upgrade_message, - upgrade_command - })) - } else { - Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127)))) - } - } - }; - drop(io_task); - result - }); - - let connection: Rc = Rc::new(OldAcpAgentConnection { - name, - connection, - child_status, - current_thread: thread_rc, - }); - - Ok(connection) + crate::acp::connect(server_name, command, &root_dir, cx).await }) } } -impl Gemini { - async fn command( - &self, - project: &Entity, - cx: &mut AsyncApp, - ) -> Result { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).gemini.clone() - })?; - - if let Some(command) = - AgentServerCommand::resolve("gemini", &[ACP_ARG], settings, &project, cx).await - { - return Ok(command); - }; - - let (fs, node_runtime) = project.update(cx, |project, _| { - (project.fs().clone(), project.node_runtime().cloned()) - })?; - let node_runtime = node_runtime.context("gemini not found on path")?; - - let directory = ::paths::agent_servers_dir().join("gemini"); - fs.create_dir(&directory).await?; - node_runtime - .npm_install_packages(&directory, &[("@google/gemini-cli", "latest")]) - .await?; - let path = directory.join("node_modules/.bin/gemini"); - - Ok(AgentServerCommand { - path, - args: vec![ACP_ARG.into()], - env: None, - }) - } - - async fn version(&self, command: &AgentServerCommand) -> Result { - let version_fut = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .arg("--version") - .kill_on_drop(true) - .output(); - - let help_fut = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .arg("--help") - .kill_on_drop(true) - .output(); - - let (version_output, help_output) = futures::future::join(version_fut, help_fut).await; - - let current_version = String::from_utf8(version_output?.stdout)?; - let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG); - - if supported { - Ok(AgentServerVersion::Supported) - } else { - Ok(AgentServerVersion::Unsupported { - error_message: format!( - "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).", - current_version - ).into(), - upgrade_message: "Upgrade Gemini to Latest".into(), - upgrade_command: "npm install -g @google/gemini-cli@latest".into(), - }) - } - } -} - #[cfg(test)] pub(crate) mod tests { use super::*; @@ -199,7 +74,7 @@ pub(crate) mod tests { AgentServerCommand { path: "node".into(), - args: vec![cli_path, ACP_ARG.into()], + args: vec![cli_path], env: None, } } diff --git a/crates/agent_servers/src/mcp_server.rs b/crates/agent_servers/src/mcp_server.rs deleted file mode 100644 index 055b89dfe2..0000000000 --- a/crates/agent_servers/src/mcp_server.rs +++ /dev/null @@ -1,207 +0,0 @@ -use acp_thread::AcpThread; -use agent_client_protocol as acp; -use anyhow::Result; -use context_server::listener::{McpServerTool, ToolResponse}; -use context_server::types::{ - Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities, - ToolsCapabilities, requests, -}; -use futures::channel::oneshot; -use gpui::{App, AsyncApp, Task, WeakEntity}; -use indoc::indoc; - -pub struct ZedMcpServer { - server: context_server::listener::McpServer, -} - -pub const SERVER_NAME: &str = "zed"; - -impl ZedMcpServer { - pub async fn new( - thread_rx: watch::Receiver>, - cx: &AsyncApp, - ) -> Result { - let mut mcp_server = context_server::listener::McpServer::new(cx).await?; - mcp_server.handle_request::(Self::handle_initialize); - - mcp_server.add_tool(RequestPermissionTool { - thread_rx: thread_rx.clone(), - }); - mcp_server.add_tool(ReadTextFileTool { - thread_rx: thread_rx.clone(), - }); - mcp_server.add_tool(WriteTextFileTool { - thread_rx: thread_rx.clone(), - }); - - Ok(Self { server: mcp_server }) - } - - pub fn server_config(&self) -> Result { - #[cfg(not(test))] - let zed_path = anyhow::Context::context( - std::env::current_exe(), - "finding current executable path for use in mcp_server", - )?; - - #[cfg(test)] - let zed_path = crate::e2e_tests::get_zed_path(); - - Ok(acp::McpServerConfig { - command: zed_path, - args: vec![ - "--nc".into(), - self.server.socket_path().display().to_string(), - ], - env: None, - }) - } - - fn handle_initialize(_: InitializeParams, cx: &App) -> Task> { - cx.foreground_executor().spawn(async move { - Ok(InitializeResponse { - protocol_version: ProtocolVersion("2025-06-18".into()), - capabilities: ServerCapabilities { - experimental: None, - logging: None, - completions: None, - prompts: None, - resources: None, - tools: Some(ToolsCapabilities { - list_changed: Some(false), - }), - }, - server_info: Implementation { - name: SERVER_NAME.into(), - version: "0.1.0".into(), - }, - meta: None, - }) - }) - } -} - -// Tools - -#[derive(Clone)] -pub struct RequestPermissionTool { - thread_rx: watch::Receiver>, -} - -impl McpServerTool for RequestPermissionTool { - type Input = acp::RequestPermissionArguments; - type Output = acp::RequestPermissionOutput; - - const NAME: &'static str = "Confirmation"; - - fn description(&self) -> &'static str { - indoc! {" - Request permission for tool calls. - - This tool is meant to be called programmatically by the agent loop, not the LLM. - "} - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result> { - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - let result = thread - .update(cx, |thread, cx| { - thread.request_tool_call_permission(input.tool_call, input.options, cx) - })? - .await; - - let outcome = match result { - Ok(option_id) => acp::RequestPermissionOutcome::Selected { option_id }, - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled, - }; - - Ok(ToolResponse { - content: vec![], - structured_content: acp::RequestPermissionOutput { outcome }, - }) - } -} - -#[derive(Clone)] -pub struct ReadTextFileTool { - thread_rx: watch::Receiver>, -} - -impl McpServerTool for ReadTextFileTool { - type Input = acp::ReadTextFileArguments; - type Output = acp::ReadTextFileOutput; - - const NAME: &'static str = "Read"; - - fn description(&self) -> &'static str { - "Reads the content of the given file in the project including unsaved changes." - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result> { - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(input.path, input.line, input.limit, false, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![], - structured_content: acp::ReadTextFileOutput { content }, - }) - } -} - -#[derive(Clone)] -pub struct WriteTextFileTool { - thread_rx: watch::Receiver>, -} - -impl McpServerTool for WriteTextFileTool { - type Input = acp::WriteTextFileArguments; - type Output = (); - - const NAME: &'static str = "Write"; - - fn description(&self) -> &'static str { - "Write to a file replacing its contents" - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result> { - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - thread - .update(cx, |thread, cx| { - thread.write_text_file(input.path, input.content, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![], - structured_content: (), - }) - } -} diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index aeb34a5e61..645674b5f1 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -13,7 +13,6 @@ pub fn init(cx: &mut App) { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, - pub codex: Option, } #[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] @@ -30,21 +29,13 @@ impl settings::Settings for AllAgentServersSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { let mut settings = AllAgentServersSettings::default(); - for AllAgentServersSettings { - gemini, - claude, - codex, - } in sources.defaults_and_customizations() - { + for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() { if gemini.is_some() { settings.gemini = gemini.clone(); } if claude.is_some() { settings.claude = claude.clone(); } - if codex.is_some() { - settings.codex = codex.clone(); - } } Ok(settings) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index e058284abc..57d3257f4d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -246,7 +246,7 @@ impl AcpThreadView { { Err(e) => { let mut cx = cx.clone(); - if e.downcast_ref::().is_some() { + if e.is::() { this.update(&mut cx, |this, cx| { this.thread_state = ThreadState::Unauthenticated { connection }; cx.notify(); @@ -719,13 +719,18 @@ impl AcpThreadView { Some(entry.diffs().map(|diff| diff.multibuffer.clone())) } - fn authenticate(&mut self, window: &mut Window, cx: &mut Context) { + fn authenticate( + &mut self, + method: acp::AuthMethodId, + window: &mut Window, + cx: &mut Context, + ) { let ThreadState::Unauthenticated { ref connection } = self.thread_state else { return; }; self.last_error.take(); - let authenticate = connection.authenticate(cx); + let authenticate = connection.authenticate(method, cx); self.auth_task = Some(cx.spawn_in(window, { let project = self.project.clone(); let agent = self.agent.clone(); @@ -2424,22 +2429,26 @@ impl Render for AcpThreadView { .on_action(cx.listener(Self::next_history_message)) .on_action(cx.listener(Self::open_agent_diff)) .child(match &self.thread_state { - ThreadState::Unauthenticated { .. } => { - v_flex() - .p_2() - .flex_1() - .items_center() - .justify_center() - .child(self.render_pending_auth_state()) - .child( - h_flex().mt_1p5().justify_center().child( - Button::new("sign-in", format!("Sign in to {}", self.agent.name())) - .on_click(cx.listener(|this, _, window, cx| { - this.authenticate(window, cx) - })), - ), - ) - } + ThreadState::Unauthenticated { connection } => v_flex() + .p_2() + .flex_1() + .items_center() + .justify_center() + .child(self.render_pending_auth_state()) + .child(h_flex().mt_1p5().justify_center().children( + connection.auth_methods().into_iter().map(|method| { + Button::new( + SharedString::from(method.id.0.clone()), + method.label.clone(), + ) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }), + )), ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)), ThreadState::LoadError(e) => v_flex() .p_2() @@ -2878,8 +2887,8 @@ mod tests { } impl AgentConnection for StubAgentConnection { - fn name(&self) -> &'static str { - "StubAgentConnection" + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] } fn new_thread( @@ -2897,17 +2906,21 @@ mod tests { .into(), ); let thread = cx - .new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx)) + .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)) .unwrap(); self.sessions.lock().insert(session_id, thread.downgrade()); Task::ready(Ok(thread)) } - fn authenticate(&self, _cx: &mut App) -> Task> { + fn authenticate( + &self, + _method_id: acp::AuthMethodId, + _cx: &mut App, + ) -> Task> { unimplemented!() } - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { let sessions = self.sessions.lock(); let thread = sessions.get(¶ms.session_id).unwrap(); let mut tasks = vec![]; @@ -2954,10 +2967,6 @@ mod tests { struct SaboteurAgentConnection; impl AgentConnection for SaboteurAgentConnection { - fn name(&self) -> &'static str { - "SaboteurAgentConnection" - } - fn new_thread( self: Rc, project: Entity, @@ -2965,15 +2974,31 @@ mod tests { cx: &mut gpui::AsyncApp, ) -> Task>> { Task::ready(Ok(cx - .new(|cx| AcpThread::new(self, project, SessionId("test".into()), cx)) + .new(|cx| { + AcpThread::new( + "SaboteurAgentConnection", + self, + project, + SessionId("test".into()), + cx, + ) + }) .unwrap())) } - fn authenticate(&self, _cx: &mut App) -> Task> { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate( + &self, + _method_id: acp::AuthMethodId, + _cx: &mut App, + ) -> Task> { unimplemented!() } - fn prompt(&self, _params: acp::PromptArguments, _cx: &mut App) -> Task> { + fn prompt(&self, _params: acp::PromptRequest, _cx: &mut App) -> Task> { Task::ready(Err(anyhow::anyhow!("Error prompting"))) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index fcb8dfbac2..a09c669769 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1987,20 +1987,6 @@ impl AgentPanel { ); }), ) - .item( - ContextMenuEntry::new("New Codex Thread") - .icon(IconName::AiOpenAi) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::Codex), - } - .boxed_clone(), - cx, - ); - }), - ) }); menu })) @@ -2662,25 +2648,6 @@ impl AgentPanel { ) }, ), - ) - .child( - NewThreadButton::new( - "new-codex-thread-btn", - "New Codex Thread", - IconName::AiOpenAi, - ) - .on_click( - |window, cx| { - window.dispatch_action( - Box::new(NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::Codex, - ), - }), - cx, - ) - }, - ), ), ) }), diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 0800031abe..c5574c2371 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -150,7 +150,6 @@ enum ExternalAgent { #[default] Gemini, ClaudeCode, - Codex, } impl ExternalAgent { @@ -158,7 +157,6 @@ impl ExternalAgent { match self { ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), - ExternalAgent::Codex => Rc::new(agent_servers::Codex), } } } diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 1eb29bbbf9..65283afa87 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -441,14 +441,12 @@ impl Client { Ok(()) } - #[allow(unused)] - pub fn on_notification(&self, method: &'static str, f: F) - where - F: 'static + Send + FnMut(Value, AsyncApp), - { - self.notification_handlers - .lock() - .insert(method, Box::new(f)); + pub fn on_notification( + &self, + method: &'static str, + f: Box, + ) { + self.notification_handlers.lock().insert(method, f); } } diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index e76e7972f7..34fa29678d 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -95,8 +95,28 @@ impl ContextServer { self.client.read().clone() } - pub async fn start(self: Arc, cx: &AsyncApp) -> Result<()> { - let client = match &self.configuration { + pub async fn start(&self, cx: &AsyncApp) -> Result<()> { + self.initialize(self.new_client(cx)?).await + } + + /// Starts the context server, making sure handlers are registered before initialization happens + pub async fn start_with_handlers( + &self, + notification_handlers: Vec<( + &'static str, + Box, + )>, + cx: &AsyncApp, + ) -> Result<()> { + let client = self.new_client(cx)?; + for (method, handler) in notification_handlers { + client.on_notification(method, handler); + } + self.initialize(client).await + } + + fn new_client(&self, cx: &AsyncApp) -> Result { + Ok(match &self.configuration { ContextServerTransport::Stdio(command, working_directory) => Client::stdio( client::ContextServerId(self.id.0.clone()), client::ModelContextServerBinary { @@ -113,8 +133,7 @@ impl ContextServer { transport.clone(), cx.clone(), )?, - }; - self.initialize(client).await + }) } async fn initialize(&self, client: Client) -> Result<()> { diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 34e3a9a78c..0e85fb2129 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -83,14 +83,18 @@ impl McpServer { } pub fn add_tool(&mut self, tool: T) { - let output_schema = schemars::schema_for!(T::Output); - let unit_schema = schemars::schema_for!(()); + let mut settings = schemars::generate::SchemaSettings::draft07(); + settings.inline_subschemas = true; + let mut generator = settings.into_generator(); + + let output_schema = generator.root_schema_for::(); + let unit_schema = generator.root_schema_for::(); let registered_tool = RegisteredTool { tool: Tool { name: T::NAME.into(), description: Some(tool.description().into()), - input_schema: schemars::schema_for!(T::Input).into(), + input_schema: generator.root_schema_for::().into(), output_schema: if output_schema == unit_schema { None } else { diff --git a/crates/context_server/src/protocol.rs b/crates/context_server/src/protocol.rs index 9ccbc8a553..5355f20f62 100644 --- a/crates/context_server/src/protocol.rs +++ b/crates/context_server/src/protocol.rs @@ -115,10 +115,11 @@ impl InitializedContextServerProtocol { self.inner.notify(T::METHOD, params) } - pub fn on_notification(&self, method: &'static str, f: F) - where - F: 'static + Send + FnMut(Value, AsyncApp), - { + pub fn on_notification( + &self, + method: &'static str, + f: Box, + ) { self.inner.on_notification(method, f); } } From 8b573d43953832b52acf057da021c4981aa01023 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Mon, 4 Aug 2025 16:56:56 +0300 Subject: [PATCH 03/46] evals: Retry on Anthropic's internal and transient I/O errors (#35395) Release Notes: - N/A --- .../assistant_tools/src/edit_agent/evals.rs | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index eda7eee0e3..9a8e762455 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1658,23 +1658,24 @@ impl EditAgentTest { } async fn retry_on_rate_limit(mut request: impl AsyncFnMut() -> Result) -> Result { + const MAX_RETRIES: usize = 20; let mut attempt = 0; + loop { attempt += 1; - match request().await { - Ok(result) => return Ok(result), - Err(err) => match err.downcast::() { - Ok(err) => match &err { + let response = request().await; + + if attempt >= MAX_RETRIES { + return response; + } + + let retry_delay = match &response { + Ok(_) => None, + Err(err) => match err.downcast_ref::() { + Some(err) => match &err { LanguageModelCompletionError::RateLimitExceeded { retry_after, .. } | LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => { - let retry_after = retry_after.unwrap_or(Duration::from_secs(5)); - // Wait for the duration supplied, with some jitter to avoid all requests being made at the same time. - let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); - eprintln!( - "Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}" - ); - Timer::after(retry_after + jitter).await; - continue; + Some(retry_after.unwrap_or(Duration::from_secs(5))) } LanguageModelCompletionError::UpstreamProviderError { status, @@ -1687,23 +1688,31 @@ async fn retry_on_rate_limit(mut request: impl AsyncFnMut() -> Result) -> StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE ) || status.as_u16() == 529; - if !should_retry { - return Err(err.into()); + if should_retry { + // Use server-provided retry_after if available, otherwise use default + Some(retry_after.unwrap_or(Duration::from_secs(5))) + } else { + None } - - // Use server-provided retry_after if available, otherwise use default - let retry_after = retry_after.unwrap_or(Duration::from_secs(5)); - let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); - eprintln!( - "Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}" - ); - Timer::after(retry_after + jitter).await; - continue; } - _ => return Err(err.into()), + LanguageModelCompletionError::ApiReadResponseError { .. } + | LanguageModelCompletionError::ApiInternalServerError { .. } + | LanguageModelCompletionError::HttpSend { .. } => { + // Exponential backoff for transient I/O and internal server errors + Some(Duration::from_secs(2_u64.pow((attempt - 1) as u32).min(30))) + } + _ => None, }, - Err(err) => return Err(err), + _ => None, }, + }; + + if let Some(retry_after) = retry_delay { + let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); + eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}"); + Timer::after(retry_after + jitter).await; + } else { + return response; } } } From 0609c8b953243b93c41e4f8453dbac6646cf333c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:58:31 -0300 Subject: [PATCH 04/46] Revise and clean up some icons (#35582) This is really just a small beginning, as there are many other icons to be revised and cleaned up. Our current set is a bit of a mess in terms of dimension, spacing, stroke width, and terminology. I'm sure there are more non-used icons I'm not covering here, too. We'll hopefully tackle it all soon leading up to 1.0. Closes https://github.com/zed-industries/zed/issues/35576 Release Notes: - N/A --- assets/icons/ai_bedrock.svg | 10 ++-- assets/icons/ai_deep_seek.svg | 4 +- assets/icons/ai_lm_studio.svg | 46 ++++++------------ assets/icons/ai_mistral.svg | 9 +++- assets/icons/ai_ollama.svg | 17 ++----- assets/icons/ai_open_ai.svg | 2 +- assets/icons/ai_open_router.svg | 14 +++--- assets/icons/ai_x_ai.svg | 2 +- assets/icons/ai_zed.svg | 9 +--- assets/icons/at_sign.svg | 1 - assets/icons/bolt.svg | 3 -- assets/icons/bolt_filled.svg | 4 +- assets/icons/bolt_filled_alt.svg | 3 -- assets/icons/bolt_outlined.svg | 3 ++ assets/icons/book_plus.svg | 1 - assets/icons/brain.svg | 1 - assets/icons/chat.svg | 4 ++ assets/icons/file_text.svg | 7 ++- assets/icons/git_onboarding_bg.svg | 40 ---------------- assets/icons/message_bubbles.svg | 6 --- assets/icons/microscope.svg | 1 - assets/icons/new_from_summary.svg | 7 --- assets/icons/play.svg | 3 -- assets/icons/play_bug.svg | 8 ---- assets/icons/play_filled.svg | 4 +- .../icons/{play_alt.svg => play_outlined.svg} | 2 +- assets/icons/reveal.svg | 1 - assets/icons/spinner.svg | 13 ----- assets/icons/strikethrough.svg | 3 -- .../{new_text_thread.svg => text_thread.svg} | 0 assets/icons/{new_thread.svg => thread.svg} | 0 assets/icons/thread_from_summary.svg | 6 +++ assets/icons/trash.svg | 6 ++- assets/icons/trash_alt.svg | 1 - assets/icons/zed_predict_bg.svg | 19 -------- crates/agent/src/context.rs | 4 +- .../manage_profiles_modal.rs | 2 +- crates/agent_ui/src/agent_panel.rs | 48 +++++++++---------- crates/agent_ui/src/context_picker.rs | 2 +- .../src/context_picker/completion_provider.rs | 4 +- .../context_picker/thread_context_picker.rs | 2 +- crates/agent_ui/src/inline_prompt_editor.rs | 2 +- crates/agent_ui/src/thread_history.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 6 +-- crates/editor/src/editor.rs | 44 +++++++++-------- crates/icons/src/icons.rs | 23 +++------ .../language_models/src/provider/lmstudio.rs | 2 +- crates/language_models/src/provider/ollama.rs | 2 +- crates/language_tools/src/lsp_tool.rs | 2 +- crates/recent_projects/src/remote_servers.rs | 2 +- crates/repl/src/notebook/cell.rs | 2 +- crates/repl/src/notebook/notebook_ui.rs | 2 +- crates/rules_library/src/rules_library.rs | 4 +- crates/tasks_ui/src/modal.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- .../ui/src/components/stories/icon_button.rs | 2 +- crates/zed/src/zed/quick_action_bar.rs | 2 +- 58 files changed, 154 insertions(+), 271 deletions(-) delete mode 100644 assets/icons/at_sign.svg delete mode 100644 assets/icons/bolt.svg delete mode 100644 assets/icons/bolt_filled_alt.svg create mode 100644 assets/icons/bolt_outlined.svg delete mode 100644 assets/icons/book_plus.svg delete mode 100644 assets/icons/brain.svg create mode 100644 assets/icons/chat.svg delete mode 100644 assets/icons/git_onboarding_bg.svg delete mode 100644 assets/icons/message_bubbles.svg delete mode 100644 assets/icons/microscope.svg delete mode 100644 assets/icons/new_from_summary.svg delete mode 100644 assets/icons/play.svg delete mode 100644 assets/icons/play_bug.svg rename assets/icons/{play_alt.svg => play_outlined.svg} (70%) delete mode 100644 assets/icons/reveal.svg delete mode 100644 assets/icons/spinner.svg delete mode 100644 assets/icons/strikethrough.svg rename assets/icons/{new_text_thread.svg => text_thread.svg} (100%) rename assets/icons/{new_thread.svg => thread.svg} (100%) create mode 100644 assets/icons/thread_from_summary.svg delete mode 100644 assets/icons/trash_alt.svg delete mode 100644 assets/icons/zed_predict_bg.svg diff --git a/assets/icons/ai_bedrock.svg b/assets/icons/ai_bedrock.svg index 2b672c364e..c9bbcc82e1 100644 --- a/assets/icons/ai_bedrock.svg +++ b/assets/icons/ai_bedrock.svg @@ -1,4 +1,8 @@ - - - + + + + + + + diff --git a/assets/icons/ai_deep_seek.svg b/assets/icons/ai_deep_seek.svg index cf480c834c..c8e5483fb3 100644 --- a/assets/icons/ai_deep_seek.svg +++ b/assets/icons/ai_deep_seek.svg @@ -1 +1,3 @@ -DeepSeek + + + diff --git a/assets/icons/ai_lm_studio.svg b/assets/icons/ai_lm_studio.svg index 0b455f48a7..5cfdeb5578 100644 --- a/assets/icons/ai_lm_studio.svg +++ b/assets/icons/ai_lm_studio.svg @@ -1,33 +1,15 @@ - - - Artboard - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/assets/icons/ai_mistral.svg b/assets/icons/ai_mistral.svg index 23b8f2ef6c..f11c177e2f 100644 --- a/assets/icons/ai_mistral.svg +++ b/assets/icons/ai_mistral.svg @@ -1 +1,8 @@ -Mistral \ No newline at end of file + + + + + + + + diff --git a/assets/icons/ai_ollama.svg b/assets/icons/ai_ollama.svg index d433df3981..36a88c1ad6 100644 --- a/assets/icons/ai_ollama.svg +++ b/assets/icons/ai_ollama.svg @@ -1,14 +1,7 @@ - - - - - - - - - - - - + + + + + diff --git a/assets/icons/ai_open_ai.svg b/assets/icons/ai_open_ai.svg index e659a472d8..e45ac315a0 100644 --- a/assets/icons/ai_open_ai.svg +++ b/assets/icons/ai_open_ai.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/ai_open_router.svg b/assets/icons/ai_open_router.svg index 94f2849146..b6f5164e0b 100644 --- a/assets/icons/ai_open_router.svg +++ b/assets/icons/ai_open_router.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/assets/icons/ai_x_ai.svg b/assets/icons/ai_x_ai.svg index 289525c8ef..d3400fbe9c 100644 --- a/assets/icons/ai_x_ai.svg +++ b/assets/icons/ai_x_ai.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/ai_zed.svg b/assets/icons/ai_zed.svg index 1c6bb8ad63..6d78efacd5 100644 --- a/assets/icons/ai_zed.svg +++ b/assets/icons/ai_zed.svg @@ -1,10 +1,3 @@ - - - - - - - - + diff --git a/assets/icons/at_sign.svg b/assets/icons/at_sign.svg deleted file mode 100644 index 4cf8cd468f..0000000000 --- a/assets/icons/at_sign.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/bolt.svg b/assets/icons/bolt.svg deleted file mode 100644 index 2688ede2a5..0000000000 --- a/assets/icons/bolt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_filled.svg b/assets/icons/bolt_filled.svg index 543e72adf8..14d8f53e02 100644 --- a/assets/icons/bolt_filled.svg +++ b/assets/icons/bolt_filled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/bolt_filled_alt.svg b/assets/icons/bolt_filled_alt.svg deleted file mode 100644 index 141e1c5f57..0000000000 --- a/assets/icons/bolt_filled_alt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_outlined.svg b/assets/icons/bolt_outlined.svg new file mode 100644 index 0000000000..58fccf7788 --- /dev/null +++ b/assets/icons/bolt_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/book_plus.svg b/assets/icons/book_plus.svg deleted file mode 100644 index 2868f07cd0..0000000000 --- a/assets/icons/book_plus.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/brain.svg b/assets/icons/brain.svg deleted file mode 100644 index 80c93814f7..0000000000 --- a/assets/icons/brain.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/chat.svg b/assets/icons/chat.svg new file mode 100644 index 0000000000..a0548c3d3e --- /dev/null +++ b/assets/icons/chat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_text.svg b/assets/icons/file_text.svg index 7c602f2ac7..a9b8f971e0 100644 --- a/assets/icons/file_text.svg +++ b/assets/icons/file_text.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/icons/git_onboarding_bg.svg b/assets/icons/git_onboarding_bg.svg deleted file mode 100644 index 18da0230a2..0000000000 --- a/assets/icons/git_onboarding_bg.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/message_bubbles.svg b/assets/icons/message_bubbles.svg deleted file mode 100644 index 03a6c7760c..0000000000 --- a/assets/icons/message_bubbles.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/microscope.svg b/assets/icons/microscope.svg deleted file mode 100644 index 2b3009a28b..0000000000 --- a/assets/icons/microscope.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/new_from_summary.svg b/assets/icons/new_from_summary.svg deleted file mode 100644 index 3b61ca51a0..0000000000 --- a/assets/icons/new_from_summary.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/play.svg b/assets/icons/play.svg deleted file mode 100644 index 2481bda7d6..0000000000 --- a/assets/icons/play.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/play_bug.svg b/assets/icons/play_bug.svg deleted file mode 100644 index 7d265dd42a..0000000000 --- a/assets/icons/play_bug.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/play_filled.svg b/assets/icons/play_filled.svg index 387304ef04..c632434305 100644 --- a/assets/icons/play_filled.svg +++ b/assets/icons/play_filled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/play_alt.svg b/assets/icons/play_outlined.svg similarity index 70% rename from assets/icons/play_alt.svg rename to assets/icons/play_outlined.svg index b327ab07b5..7e1cacd5af 100644 --- a/assets/icons/play_alt.svg +++ b/assets/icons/play_outlined.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/reveal.svg b/assets/icons/reveal.svg deleted file mode 100644 index ff5444d8f8..0000000000 --- a/assets/icons/reveal.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/spinner.svg b/assets/icons/spinner.svg deleted file mode 100644 index 4f4034ae89..0000000000 --- a/assets/icons/spinner.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/strikethrough.svg b/assets/icons/strikethrough.svg deleted file mode 100644 index d7d0905912..0000000000 --- a/assets/icons/strikethrough.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/new_text_thread.svg b/assets/icons/text_thread.svg similarity index 100% rename from assets/icons/new_text_thread.svg rename to assets/icons/text_thread.svg diff --git a/assets/icons/new_thread.svg b/assets/icons/thread.svg similarity index 100% rename from assets/icons/new_thread.svg rename to assets/icons/thread.svg diff --git a/assets/icons/thread_from_summary.svg b/assets/icons/thread_from_summary.svg new file mode 100644 index 0000000000..7519935aff --- /dev/null +++ b/assets/icons/thread_from_summary.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg index b71035b99c..1322e90f9f 100644 --- a/assets/icons/trash.svg +++ b/assets/icons/trash.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/trash_alt.svg b/assets/icons/trash_alt.svg deleted file mode 100644 index 6867b42147..0000000000 --- a/assets/icons/trash_alt.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/zed_predict_bg.svg b/assets/icons/zed_predict_bg.svg deleted file mode 100644 index 1dccbb51af..0000000000 --- a/assets/icons/zed_predict_bg.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index ddd13de491..cd366b8308 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -42,8 +42,8 @@ impl ContextKind { ContextKind::Symbol => IconName::Code, ContextKind::Selection => IconName::Context, ContextKind::FetchedUrl => IconName::Globe, - ContextKind::Thread => IconName::MessageBubbles, - ContextKind::TextThread => IconName::MessageBubbles, + ContextKind::Thread => IconName::Thread, + ContextKind::TextThread => IconName::TextThread, ContextKind::Rules => RULES_ICON, ContextKind::Image => IconName::Image, } diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index 45536ff13b..5d44bb2d92 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -483,7 +483,7 @@ impl ManageProfilesModal { let icon = match mode.profile_id.as_str() { "write" => IconName::Pencil, - "ask" => IconName::MessageBubbles, + "ask" => IconName::Chat, _ => IconName::UserRoundPen, }; diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index a09c669769..b552a701f0 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1911,27 +1911,6 @@ impl AgentPanel { .when(cx.has_flag::(), |this| { this.header("Zed Agent") }) - .item( - ContextMenuEntry::new("New Thread") - .icon(IconName::NewThread) - .icon_color(Color::Muted) - .action(NewThread::default().boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Text Thread") - .icon(IconName::NewTextThread) - .icon_color(Color::Muted) - .action(NewTextThread.boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action(NewTextThread.boxed_clone(), cx); - }), - ) .when_some(active_thread, |this, active_thread| { let thread = active_thread.read(cx); @@ -1939,7 +1918,7 @@ impl AgentPanel { let thread_id = thread.id().clone(); this.item( ContextMenuEntry::new("New From Summary") - .icon(IconName::NewFromSummary) + .icon(IconName::ThreadFromSummary) .icon_color(Color::Muted) .handler(move |window, cx| { window.dispatch_action( @@ -1954,6 +1933,27 @@ impl AgentPanel { this } }) + .item( + ContextMenuEntry::new("New Thread") + .icon(IconName::Thread) + .icon_color(Color::Muted) + .action(NewThread::default().boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action( + NewThread::default().boxed_clone(), + cx, + ); + }), + ) + .item( + ContextMenuEntry::new("New Text Thread") + .icon(IconName::TextThread) + .icon_color(Color::Muted) + .action(NewTextThread.boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + }), + ) .when(cx.has_flag::(), |this| { this.separator() .header("External Agents") @@ -2558,7 +2558,7 @@ impl AgentPanel { NewThreadButton::new( "new-thread-btn", "New Thread", - IconName::NewThread, + IconName::Thread, ) .keybinding(KeyBinding::for_action_in( &NewThread::default(), @@ -2579,7 +2579,7 @@ impl AgentPanel { NewThreadButton::new( "new-text-thread-btn", "New Text Thread", - IconName::NewTextThread, + IconName::TextThread, ) .keybinding(KeyBinding::for_action_in( &NewTextThread, diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 5cc56b014e..32f9a096d9 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -148,7 +148,7 @@ impl ContextPickerMode { Self::File => IconName::File, Self::Symbol => IconName::Code, Self::Fetch => IconName::Globe, - Self::Thread => IconName::MessageBubbles, + Self::Thread => IconName::Thread, Self::Rules => RULES_ICON, } } diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index b377e40b19..5ca0913be7 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -423,7 +423,7 @@ impl ContextPickerCompletionProvider { let icon_for_completion = if recent { IconName::HistoryRerun } else { - IconName::MessageBubbles + IconName::Thread }; let new_text = format!("{} ", MentionLink::for_thread(&thread_entry)); let new_text_len = new_text.len(); @@ -436,7 +436,7 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_for_completion.path().into()), confirm: Some(confirm_completion_callback( - IconName::MessageBubbles.path().into(), + IconName::Thread.path().into(), thread_entry.title().clone(), excerpt_id, source_range.start, diff --git a/crates/agent_ui/src/context_picker/thread_context_picker.rs b/crates/agent_ui/src/context_picker/thread_context_picker.rs index cb2e97a493..15cc731f8f 100644 --- a/crates/agent_ui/src/context_picker/thread_context_picker.rs +++ b/crates/agent_ui/src/context_picker/thread_context_picker.rs @@ -253,7 +253,7 @@ pub fn render_thread_context_entry( .gap_1p5() .max_w_72() .child( - Icon::new(IconName::MessageBubbles) + Icon::new(IconName::Thread) .size(IconSize::XSmall) .color(Color::Muted), ) diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index ade7a5e13d..a5f90edb57 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -541,7 +541,7 @@ impl PromptEditor { match &self.mode { PromptEditorMode::Terminal { .. } => vec![ accept, - IconButton::new("confirm", IconName::Play) + IconButton::new("confirm", IconName::PlayOutlined) .icon_color(Color::Info) .shape(IconButtonShape::Square) .tooltip(|window, cx| { diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index a2ee816f73..b8d1db88d6 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -701,7 +701,7 @@ impl RenderOnce for HistoryEntryElement { .on_hover(self.on_hover) .end_slot::(if self.hovered || self.selected { Some( - IconButton::new("delete", IconName::TrashAlt) + IconButton::new("delete", IconName::Trash) .shape(IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 3e2d813f1b..3a9b568264 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -1162,7 +1162,7 @@ impl Panel for ChatPanel { } fn icon(&self, _window: &Window, cx: &App) -> Option { - self.enabled(cx).then(|| ui::IconName::MessageBubbles) + self.enabled(cx).then(|| ui::IconName::Chat) } fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 54077303a1..689591df12 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1124,7 +1124,7 @@ impl CollabPanel { .relative() .gap_1() .child(render_tree_branch(false, false, window, cx)) - .child(IconButton::new(0, IconName::MessageBubbles)) + .child(IconButton::new(0, IconName::Chat)) .children(has_messages_notification.then(|| { div() .w_1p5() @@ -2923,7 +2923,7 @@ impl CollabPanel { .gap_1() .px_1() .child( - IconButton::new("channel_chat", IconName::MessageBubbles) + IconButton::new("channel_chat", IconName::Chat) .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) @@ -2939,7 +2939,7 @@ impl CollabPanel { .visible_on_hover(""), ) .child( - IconButton::new("channel_notes", IconName::File) + IconButton::new("channel_notes", IconName::FileText) .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 49484ed137..e4628b43aa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6403,7 +6403,6 @@ impl Editor { IconButton::new("inline_code_actions", ui::IconName::BoltFilled) .icon_size(icon_size) .shape(ui::IconButtonShape::Square) - .style(ButtonStyle::Transparent) .icon_color(ui::Color::Hidden) .toggle_state(is_active) .when(show_tooltip, |this| { @@ -8338,26 +8337,29 @@ impl Editor { let color = Color::Muted; let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor); - IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(color) - .toggle_state(is_active) - .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { - let quick_launch = e.down.button == MouseButton::Left; - window.focus(&editor.focus_handle(cx)); - editor.toggle_code_actions( - &ToggleCodeActions { - deployed_from: Some(CodeActionSource::RunMenu(row)), - quick_launch, - }, - window, - cx, - ); - })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); - })) + IconButton::new( + ("run_indicator", row.0 as usize), + ui::IconName::PlayOutlined, + ) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(color) + .toggle_state(is_active) + .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { + let quick_launch = e.down.button == MouseButton::Left; + window.focus(&editor.focus_handle(cx)); + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from: Some(CodeActionSource::RunMenu(row)), + quick_launch, + }, + window, + cx, + ); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); + })) } pub fn context_menu_visible(&self) -> bool { diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index fe68cdd2d6..a94d89bdc8 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -38,7 +38,6 @@ pub enum IconName { ArrowUpFromLine, ArrowUpRight, ArrowUpRightAlt, - AtSign, AudioOff, AudioOn, Backspace, @@ -48,15 +47,13 @@ pub enum IconName { BellRing, Binary, Blocks, - Bolt, + BoltOutlined, BoltFilled, - BoltFilledAlt, Book, BookCopy, - BookPlus, - Brain, BugOff, CaseSensitive, + Chat, Check, CheckDouble, ChevronDown, @@ -184,14 +181,9 @@ pub enum IconName { Maximize, Menu, MenuAlt, - MessageBubbles, Mic, MicMute, - Microscope, Minimize, - NewFromSummary, - NewTextThread, - NewThread, Option, PageDown, PageUp, @@ -202,9 +194,7 @@ pub enum IconName { PersonCircle, PhoneIncoming, Pin, - Play, - PlayAlt, - PlayBug, + PlayOutlined, PlayFilled, Plus, PocketKnife, @@ -221,7 +211,6 @@ pub enum IconName { ReplyArrowRight, Rerun, Return, - Reveal, RotateCcw, RotateCw, Route, @@ -246,7 +235,6 @@ pub enum IconName { Sparkle, SparkleAlt, SparkleFilled, - Spinner, Split, SplitAlt, SquareDot, @@ -256,7 +244,6 @@ pub enum IconName { StarFilled, Stop, StopFilled, - Strikethrough, Supermaven, SupermavenDisabled, SupermavenError, @@ -266,6 +253,9 @@ pub enum IconName { Terminal, TerminalAlt, TextSnippet, + TextThread, + Thread, + ThreadFromSummary, ThumbsDown, ThumbsUp, TodoComplete, @@ -285,7 +275,6 @@ pub enum IconName { ToolTerminal, ToolWeb, Trash, - TrashAlt, Triangle, TriangleRight, Undo, diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 01600f3646..9792b4f27b 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -744,7 +744,7 @@ impl Render for ConfigurationView { Button::new("retry_lmstudio_models", "Connect") .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .icon(IconName::Play) + .icon(IconName::PlayOutlined) .on_click(cx.listener(move |this, _, _window, cx| { this.retry_connection(cx) })), diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index c20ea0ee1e..d4739bcab8 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -658,7 +658,7 @@ impl Render for ConfigurationView { Button::new("retry_ollama_models", "Connect") .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .icon(IconName::Play) + .icon(IconName::PlayOutlined) .on_click(cx.listener(move |this, _, _, cx| { this.retry_connection(cx) })), diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index a339f3b941..50547253a9 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -1015,7 +1015,7 @@ impl Render for LspTool { .anchor(Corner::BottomLeft) .with_handle(self.popover_menu_handle.clone()) .trigger_with_tooltip( - IconButton::new("zed-lsp-tool-button", IconName::Bolt) + IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined) .when_some(indicator, IconButton::indicator) .icon_size(IconSize::Small) .indicator_border_color(Some(cx.theme().colors().status_bar_background)), diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index aa5103e62b..655e24860a 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -963,7 +963,7 @@ impl RemoteServerProjects { .child({ let project = project.clone(); // Right-margin to offset it from the Scrollbar - IconButton::new("remove-remote-project", IconName::TrashAlt) + IconButton::new("remove-remote-project", IconName::Trash) .icon_size(IconSize::Small) .shape(IconButtonShape::Square) .size(ButtonSize::Large) diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 2ed68c17d1..18851417c0 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -38,7 +38,7 @@ pub enum CellControlType { impl CellControlType { fn icon_name(&self) -> IconName { match self { - CellControlType::RunCell => IconName::Play, + CellControlType::RunCell => IconName::PlayOutlined, CellControlType::RerunCell => IconName::ArrowCircle, CellControlType::ClearCell => IconName::ListX, CellControlType::CellOptions => IconName::Ellipsis, diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index d14f458fa9..3e96cc4d11 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -343,7 +343,7 @@ impl NotebookEditor { .child( Self::render_notebook_control( "run-all-cells", - IconName::Play, + IconName::PlayOutlined, window, cx, ) diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index be6a69c23b..2f77b4f3cc 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -319,7 +319,7 @@ impl PickerDelegate for RulePickerDelegate { }) .into_any() } else { - IconButton::new("delete-rule", IconName::TrashAlt) + IconButton::new("delete-rule", IconName::Trash) .icon_color(Color::Muted) .icon_size(IconSize::Small) .shape(IconButtonShape::Square) @@ -1163,7 +1163,7 @@ impl RulesLibrary { }) .into_any() } else { - IconButton::new("delete-rule", IconName::TrashAlt) + IconButton::new("delete-rule", IconName::Trash) .icon_size(IconSize::Small) .tooltip(move |window, cx| { Tooltip::for_action( diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 1510f613e3..c4b0931c35 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -500,7 +500,7 @@ impl PickerDelegate for TasksModalDelegate { .map(|icon| icon.color(Color::Muted).size(IconSize::Small)); let indicator = if matches!(source_kind, TaskSourceKind::Lsp { .. }) { Some(Indicator::icon( - Icon::new(IconName::Bolt).size(IconSize::Small), + Icon::new(IconName::BoltOutlined).size(IconSize::Small), )) } else { None diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index bf65a736e8..2e6be5aaf4 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1591,7 +1591,7 @@ impl Item for TerminalView { let (icon, icon_color, rerun_button) = match terminal.task() { Some(terminal_task) => match &terminal_task.status { TaskStatus::Running => ( - IconName::Play, + IconName::PlayOutlined, Color::Disabled, TerminalView::rerun_button(&terminal_task), ), diff --git a/crates/ui/src/components/stories/icon_button.rs b/crates/ui/src/components/stories/icon_button.rs index e787e81b55..ad6886252d 100644 --- a/crates/ui/src/components/stories/icon_button.rs +++ b/crates/ui/src/components/stories/icon_button.rs @@ -77,7 +77,7 @@ impl Render for IconButtonStory { let with_tooltip_button = StoryItem::new( "With `tooltip`", - IconButton::new("with_tooltip_button", IconName::MessageBubbles) + IconButton::new("with_tooltip_button", IconName::Chat) .tooltip(Tooltip::text("Open messages")), ) .description("Displays an icon button that has a tooltip when hovered.") diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index aff124a0bc..1164704ce6 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -192,7 +192,7 @@ impl Render for QuickActionBar { }; v_flex() .child( - IconButton::new("toggle_code_actions_icon", IconName::Bolt) + IconButton::new("toggle_code_actions_icon", IconName::BoltOutlined) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .disabled(!has_available_code_actions) From 5f77c6a68fdc7f982a00d02d539cacbf017fbd1c Mon Sep 17 00:00:00 2001 From: Joshua Byrd Date: Tue, 5 Aug 2025 00:58:41 +1000 Subject: [PATCH 05/46] docs: Rewrite the OpenAI compatible API section (#35558) This PR updates the OpenAI compatible API section clarifying that API keys aren't stored in the `settings.json`. It also updates the JSON as some fields are not available anymore. Release Notes: - docs: Updated the OpenAI compatible API section to clarify API keys aren't stored in your `settings.json`. --------- Co-authored-by: Danilo Leal --- docs/src/ai/llm-providers.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index bd208e94ac..a6e6f7c774 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -441,30 +441,26 @@ Custom models will be listed in the model dropdown in the Agent Panel. ### OpenAI API Compatible {#openai-api-compatible} -Zed supports using [OpenAI compatible APIs](https://platform.openai.com/docs/api-reference/chat) by specifying a custom `api_url` and `available_models` for the OpenAI provider. This is useful for connecting to other hosted services (like Together AI, Anyscale, etc.) or local models. +Zed supports using [OpenAI compatible APIs](https://platform.openai.com/docs/api-reference/chat) by specifying a custom `api_url` and `available_models` for the OpenAI provider. +This is useful for connecting to other hosted services (like Together AI, Anyscale, etc.) or local models. -To configure a compatible API, you can add a custom API URL for OpenAI either via the UI (currently available only in Preview) or by editing your `settings.json`. +You can add a custom, OpenAI-compatible model via either via the UI or by editing your `settings.json`. -For example, to connect to [Together AI](https://www.together.ai/) via the UI: +To do it via the UI, go to the Agent Panel settings (`agent: open settings`) and look for the "Add Provider" button to the right of the "LLM Providers" section title. +Then, fill up the input fields available in the modal. -1. Get an API key from your [Together AI account](https://api.together.ai/settings/api-keys). -2. Go to the Agent Panel's settings view, click on the "Add Provider" button, and then on the "OpenAI" menu item -3. Add the requested fields, such as `api_url`, `api_key`, available models, and others - -Alternatively, you can also add it via the `settings.json`: +To do it via your `settings.json`, add the following snippet under `language_models`: ```json { "language_models": { "openai": { - "api_url": "https://api.together.xyz/v1", - "api_key": "YOUR_TOGETHER_AI_API_KEY", + "api_url": "https://api.together.xyz/v1", // Using Together AI as an example "available_models": [ { "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", "display_name": "Together Mixtral 8x7B", - "max_tokens": 32768, - "supports_tools": true + "max_tokens": 32768 } ] } @@ -472,6 +468,9 @@ Alternatively, you can also add it via the `settings.json`: } ``` +Note that LLM API keys aren't stored in your settings file. +So, ensure you have it set in your environment variables (`OPENAI_API_KEY=`) so your settings can pick it up. + ### OpenRouter {#openrouter} > ✅ Supports tool use From a6a34dad0fd82dd945fbe799209195483afdb770 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 4 Aug 2025 12:02:22 -0300 Subject: [PATCH 06/46] Fix gemini e2e tests (#35583) Release Notes: - N/A --- crates/agent_servers/src/e2e_tests.rs | 6 +++--- crates/agent_servers/src/gemini.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 16bf1e6b47..a60aefb7b9 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -150,7 +150,7 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp drop(tempdir); } -pub async fn test_tool_call_with_confirmation( +pub async fn test_tool_call_with_permission( server: impl AgentServer + 'static, allow_option_id: acp::PermissionOptionId, cx: &mut TestAppContext, @@ -337,8 +337,8 @@ macro_rules! common_e2e_tests { #[::gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] - async fn tool_call_with_confirmation(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_tool_call_with_confirmation( + async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) { + $crate::e2e_tests::test_tool_call_with_permission( $server, ::agent_client_protocol::PermissionOptionId($allow_option_id.into()), cx, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 1119a8b4ee..2366783d22 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -64,7 +64,7 @@ pub(crate) mod tests { use crate::AgentServerCommand; use std::path::Path; - crate::common_e2e_tests!(Gemini, allow_option_id = "0"); + crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once"); pub fn local_command() -> AgentServerCommand { let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) From bb5af6f76d1042feb75644b986b560909a38ae5e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 4 Aug 2025 11:37:34 -0400 Subject: [PATCH 07/46] Fix escape in terminal with JetBrains keymap (#35585) Closes https://github.com/zed-industries/zed/issues/35429 Closes https://github.com/zed-industries/zed/issues/35091 Follow-up to: https://github.com/zed-industries/zed/pull/35230 Release Notes: - Fix `escape` in Terminal broken in JetBrains compatability keymaps --- assets/keymaps/linux/jetbrains.json | 2 +- assets/keymaps/macos/jetbrains.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index 9bc1f24bfb..3df1243fed 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -166,7 +166,7 @@ { "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } }, { "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } }, { - "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", + "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "bindings": { "escape": "editor::ToggleFocus", "shift-escape": "workspace::CloseActiveDock" diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index b1cd51a338..66962811f4 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -167,7 +167,7 @@ { "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } }, { "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } }, { - "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", + "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "bindings": { "escape": "editor::ToggleFocus", "shift-escape": "workspace::CloseActiveDock" From d577ef52cb2a62f40063fba3f91a9b2e5d4b66a8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:44:29 -0300 Subject: [PATCH 08/46] thread view: Scroll to the bottom when sending new messages + adjust controls display (#35586) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 120 +++++++++++++++---------- 1 file changed, 71 insertions(+), 49 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 57d3257f4d..24d8b73396 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -424,11 +424,14 @@ impl AcpThreadView { let mention_set = self.mention_set.clone(); self.set_editor_is_expanded(false, cx); + self.message_editor.update(cx, |editor, cx| { editor.clear(window, cx); editor.remove_creases(mention_set.lock().drain(), cx) }); + self.scroll_to_bottom(cx); + self.message_history.borrow_mut().push(chunks); } @@ -2022,15 +2025,15 @@ impl AcpThreadView { .icon_color(Color::Accent) .style(ButtonStyle::Filled) .disabled(self.thread().is_none() || is_editor_empty) - .on_click(cx.listener(|this, _, window, cx| { - this.chat(&Chat, window, cx); - })) .when(!is_editor_empty, |button| { button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx)) }) .when(is_editor_empty, |button| { button.tooltip(Tooltip::text("Type a message to submit")) }) + .on_click(cx.listener(|this, _, window, cx| { + this.chat(&Chat, window, cx); + })) .into_any_element() } else { IconButton::new("stop-generation", IconName::StopFilled) @@ -2245,6 +2248,14 @@ impl AcpThreadView { cx.notify(); } + pub fn scroll_to_bottom(&mut self, cx: &mut Context) { + if let Some(thread) = self.thread() { + let entry_count = thread.read(cx).entries().len(); + self.list_state.reset(entry_count); + cx.notify(); + } + } + fn notify_with_sound( &mut self, caption: impl Into, @@ -2392,17 +2403,9 @@ impl AcpThreadView { self.notification_subscriptions.remove(&window); } } -} -impl Focusable for AcpThreadView { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.message_editor.focus_handle(cx) - } -} - -impl Render for AcpThreadView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText) + fn render_thread_controls(&mut self, cx: &mut Context) -> impl IntoElement { + let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileText) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Open Thread as Markdown")) @@ -2421,6 +2424,28 @@ impl Render for AcpThreadView { this.scroll_to_top(cx); })); + h_flex() + .mt_1() + .mr_1() + .py_2() + .px(RESPONSE_PADDING_X) + .opacity(0.4) + .hover(|style| style.opacity(1.)) + .flex_wrap() + .justify_end() + .child(open_as_markdown) + .child(scroll_to_top) + } +} + +impl Focusable for AcpThreadView { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.message_editor.focus_handle(cx) + } +} + +impl Render for AcpThreadView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .key_context("AcpThread") @@ -2456,42 +2481,39 @@ impl Render for AcpThreadView { .items_center() .justify_center() .child(self.render_error_state(e, cx)), - ThreadState::Ready { thread, .. } => v_flex().flex_1().map(|this| { - if self.list_state.item_count() > 0 { - this.child( - list(self.list_state.clone()) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), - ) - .child( - h_flex() - .group("controls") - .mt_1() - .mr_1() - .py_2() - .px(RESPONSE_PADDING_X) - .opacity(0.4) - .hover(|style| style.opacity(1.)) - .flex_wrap() - .justify_end() - .child(open_as_markdown) - .child(scroll_to_top) - .into_any_element(), - ) - .children(match thread.read(cx).status() { - ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None, - ThreadStatus::Generating => div() - .px_5() - .py_2() - .child(LoadingLabel::new("").size(LabelSize::Small)) - .into(), - }) - .children(self.render_activity_bar(&thread, window, cx)) - } else { - this.child(self.render_empty_state(cx)) - } - }), + ThreadState::Ready { thread, .. } => { + let thread_clone = thread.clone(); + + v_flex().flex_1().map(|this| { + if self.list_state.item_count() > 0 { + let is_generating = + matches!(thread_clone.read(cx).status(), ThreadStatus::Generating); + + this.child( + list(self.list_state.clone()) + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any(), + ) + .when(!is_generating, |this| { + this.child(self.render_thread_controls(cx)) + }) + .children(match thread_clone.read(cx).status() { + ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { + None + } + ThreadStatus::Generating => div() + .px_5() + .py_2() + .child(LoadingLabel::new("").size(LabelSize::Small)) + .into(), + }) + .children(self.render_activity_bar(&thread_clone, window, cx)) + } else { + this.child(self.render_empty_state(cx)) + } + }) + } }) .when_some(self.last_error.clone(), |el, error| { el.child( From 899bc8a8fd9c82565cc1b37659b226587b699929 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 4 Aug 2025 09:45:11 -0600 Subject: [PATCH 09/46] Fix edit prediction disablement with `"disable_ai": true` setting (#35513) Even after #35327 edit predictions were still being queried and shown after setting `"disable_ai": true` Also moves `DisableAiSettings` to the `project` crate so that it gets included in tests via existing use of `Project::init_settings(cx)`. Release Notes: - Fixed `"disable_ai": true` setting disabling edit predictions. --- Cargo.lock | 1 - crates/agent_ui/src/agent_panel.rs | 4 +-- crates/agent_ui/src/agent_ui.rs | 3 +- crates/agent_ui/src/inline_assistant.rs | 4 +-- crates/client/src/client.rs | 28 ----------------- crates/copilot/src/copilot.rs | 2 +- crates/editor/src/editor.rs | 15 ++++++++-- crates/git_ui/Cargo.toml | 1 - crates/git_ui/src/commit_modal.rs | 2 +- crates/git_ui/src/git_panel.rs | 7 ++--- crates/inline_completion_button/Cargo.toml | 1 + .../src/inline_completion_button.rs | 3 +- crates/onboarding/src/ai_setup_page.rs | 6 ++-- crates/project/src/project.rs | 30 ++++++++++++++++++- crates/welcome/src/welcome.rs | 3 +- crates/zed/src/zed/quick_action_bar.rs | 2 +- crates/zeta/src/init.rs | 2 +- 17 files changed, 60 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56210557d2..7021506502 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6315,7 +6315,6 @@ dependencies = [ "buffer_diff", "call", "chrono", - "client", "cloud_llm_client", "collections", "command_palette_hooks", diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index b552a701f0..4751eff15e 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -43,7 +43,7 @@ use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; -use client::{DisableAiSettings, UserStore, zed_urls}; +use client::{UserStore, zed_urls}; use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use feature_flags::{self, FeatureFlagAppExt}; @@ -58,7 +58,7 @@ use language::LanguageRegistry; use language_model::{ ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, }; -use project::{Project, ProjectPath, Worktree}; +use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; use search::{BufferSearchBar, buffer_search}; diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index c5574c2371..30faf5ef2e 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -31,7 +31,7 @@ use std::sync::Arc; use agent::{Thread, ThreadId}; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; -use client::{Client, DisableAiSettings}; +use client::Client; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use fs::Fs; @@ -40,6 +40,7 @@ use language::LanguageRegistry; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, }; +use project::DisableAiSettings; use prompt_store::PromptBuilder; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index ffa654d12b..159ccd0635 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -16,7 +16,7 @@ use agent::{ }; use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; -use client::{DisableAiSettings, telemetry::Telemetry}; +use client::telemetry::Telemetry; use collections::{HashMap, HashSet, VecDeque, hash_map}; use editor::SelectionEffects; use editor::{ @@ -39,7 +39,7 @@ use language_model::{ }; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; -use project::{CodeAction, LspAction, Project, ProjectTransaction}; +use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction}; use prompt_store::{PromptBuilder, PromptStore}; use settings::{Settings, SettingsStore}; use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e6d8f10d12..309e4d892f 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -150,7 +150,6 @@ impl Settings for ProxySettings { pub fn init_settings(cx: &mut App) { TelemetrySettings::register(cx); - DisableAiSettings::register(cx); ClientSettings::register(cx); ProxySettings::register(cx); } @@ -539,33 +538,6 @@ impl settings::Settings for TelemetrySettings { } } -/// Whether to disable all AI features in Zed. -/// -/// Default: false -#[derive(Copy, Clone, Debug)] -pub struct DisableAiSettings { - pub disable_ai: bool, -} - -impl settings::Settings for DisableAiSettings { - const KEY: Option<&'static str> = Some("disable_ai"); - - type FileContent = Option; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - Ok(Self { - disable_ai: sources - .user - .or(sources.server) - .copied() - .flatten() - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), - }) - } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} -} - impl Client { pub fn new( clock: Arc, diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index cacf834e0d..efe6fb743a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -6,7 +6,6 @@ mod sign_in; use crate::sign_in::initiate_sign_in_within_workspace; use ::fs::Fs; use anyhow::{Context as _, Result, anyhow}; -use client::DisableAiSettings; use collections::{HashMap, HashSet}; use command_palette_hooks::CommandPaletteFilter; use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared}; @@ -24,6 +23,7 @@ use language::{ use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; use node_runtime::NodeRuntime; use parking_lot::Mutex; +use project::DisableAiSettings; use request::StatusNotification; use serde_json::json; use settings::Settings; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e4628b43aa..8e1efc0701 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -83,7 +83,7 @@ use aho_corasick::AhoCorasick; use anyhow::{Context as _, Result, anyhow}; use blink_manager::BlinkManager; use buffer_diff::DiffHunkStatus; -use client::{Collaborator, DisableAiSettings, ParticipantIndex}; +use client::{Collaborator, ParticipantIndex}; use clock::{AGENT_REPLICA_ID, ReplicaId}; use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, @@ -148,8 +148,8 @@ use parking_lot::Mutex; use persistence::DB; use project::{ BreakpointWithPosition, CodeAction, Completion, CompletionIntent, CompletionResponse, - CompletionSource, DocumentHighlight, InlayHint, Location, LocationLink, PrepareRenameResponse, - Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind, + CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, Location, LocationLink, + PrepareRenameResponse, Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind, debugger::breakpoint_store::Breakpoint, debugger::{ breakpoint_store::{ @@ -6995,6 +6995,10 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option<()> { + if DisableAiSettings::get_global(cx).disable_ai { + return None; + } + let provider = self.edit_prediction_provider()?; let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = @@ -7052,6 +7056,7 @@ impl Editor { pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai { self.edit_prediction_settings = EditPredictionSettings::Disabled; + self.discard_inline_completion(false, cx); } else { let selection = self.selections.newest_anchor(); let cursor = selection.head(); @@ -7669,6 +7674,10 @@ impl Editor { _window: &mut Window, cx: &mut Context, ) -> Option<()> { + if DisableAiSettings::get_global(cx).disable_ai { + return None; + } + let selection = self.selections.newest_anchor(); let cursor = selection.head(); let multibuffer = self.buffer.read(cx).snapshot(cx); diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 4c919249ee..e6547e7ae9 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -23,7 +23,6 @@ askpass.workspace = true buffer_diff.workspace = true call.workspace = true chrono.workspace = true -client.workspace = true cloud_llm_client.workspace = true collections.workspace = true command_palette_hooks.workspace = true diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 88ec2dc84e..5dfa800ae5 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -1,9 +1,9 @@ use crate::branch_picker::{self, BranchList}; use crate::git_panel::{GitPanel, commit_message_editor}; -use client::DisableAiSettings; use git::repository::CommitOptions; use git::{Amend, Commit, GenerateCommitMessage, Signoff}; use panel::{panel_button, panel_editor_style}; +use project::DisableAiSettings; use settings::Settings; use ui::{ ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*, diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ee74ac4d54..344fa86142 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -12,7 +12,6 @@ use crate::{ use agent_settings::AgentSettings; use anyhow::Context as _; use askpass::AskPassDelegate; -use client::DisableAiSettings; use db::kvp::KEY_VALUE_STORE; use editor::{ Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar, @@ -51,10 +50,9 @@ use panel::{ PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button, }; -use project::git_store::{RepositoryEvent, RepositoryId}; use project::{ - Fs, Project, ProjectPath, - git_store::{GitStoreEvent, Repository}, + DisableAiSettings, Fs, Project, ProjectPath, + git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId}, }; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -5115,7 +5113,6 @@ mod tests { language::init(cx); editor::init(cx); Project::init_settings(cx); - client::DisableAiSettings::register(cx); crate::init(cx); }); } diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index b34e59336b..7b6ae43465 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -25,6 +25,7 @@ indoc.workspace = true inline_completion.workspace = true language.workspace = true paths.workspace = true +project.workspace = true regex.workspace = true settings.workspace = true supermaven.workspace = true diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 2d7f211942..79ebc573df 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use client::{DisableAiSettings, UserStore, zed_urls}; +use client::{UserStore, zed_urls}; use cloud_llm_client::UsageLimit; use copilot::{Copilot, Status}; use editor::{ @@ -19,6 +19,7 @@ use language::{ EditPredictionsMode, File, Language, language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings}, }; +use project::DisableAiSettings; use regex::Regex; use settings::{Settings, SettingsStore, update_settings_file}; use std::{ diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 2f031e7bb8..b4b043196b 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -1,22 +1,20 @@ use std::sync::Arc; use ai_onboarding::{AiUpsellCard, SignInStatus}; -use client::DisableAiSettings; use fs::Fs; use gpui::{ Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*, }; use itertools; - use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; +use project::DisableAiSettings; use settings::{Settings, update_settings_file}; use ui::{ Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState, prelude::*, }; -use workspace::ModalView; - use util::ResultExt; +use workspace::ModalView; use zed_actions::agent::OpenSettings; use crate::Onboarding; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 623f48d3c9..5000ba93be 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -97,7 +97,7 @@ use rpc::{ }; use search::{SearchInputKind, SearchQuery, SearchResult}; use search_history::SearchHistory; -use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore}; +use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsSources, SettingsStore}; use smol::channel::Receiver; use snippet::Snippet; use snippet_provider::SnippetProvider; @@ -942,10 +942,38 @@ pub enum PulledDiagnostics { }, } +/// Whether to disable all AI features in Zed. +/// +/// Default: false +#[derive(Copy, Clone, Debug)] +pub struct DisableAiSettings { + pub disable_ai: bool, +} + +impl settings::Settings for DisableAiSettings { + const KEY: Option<&'static str> = Some("disable_ai"); + + type FileContent = Option; + + fn load(sources: SettingsSources, _: &mut App) -> Result { + Ok(Self { + disable_ai: sources + .user + .or(sources.server) + .copied() + .flatten() + .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), + }) + } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} +} + impl Project { pub fn init_settings(cx: &mut App) { WorktreeSettings::register(cx); ProjectSettings::register(cx); + DisableAiSettings::register(cx); } pub fn init(client: &Arc, cx: &mut App) { diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 352118eee8..b0a1c316f4 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,10 +1,11 @@ -use client::{DisableAiSettings, TelemetrySettings, telemetry::Telemetry}; +use client::{TelemetrySettings, telemetry::Telemetry}; use db::kvp::KEY_VALUE_STORE; use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg, }; use language::language_settings::{EditPredictionProvider, all_language_settings}; +use project::DisableAiSettings; use settings::{Settings, SettingsStore}; use std::sync::Arc; use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*}; diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 1164704ce6..7ab7293573 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -2,7 +2,6 @@ mod preview; mod repl_menu; use agent_settings::AgentSettings; -use client::DisableAiSettings; use editor::actions::{ AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic, GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll, @@ -16,6 +15,7 @@ use gpui::{ FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, anchored, deferred, point, }; +use project::DisableAiSettings; use project::project_settings::DiagnosticSeverity; use search::{BufferSearchBar, buffer_search}; use settings::{Settings, SettingsStore}; diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index 4a65771223..a01e3a89a2 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -1,10 +1,10 @@ use std::any::{Any, TypeId}; -use client::DisableAiSettings; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag}; use gpui::actions; use language::language_settings::{AllLanguageSettings, EditPredictionProvider}; +use project::DisableAiSettings; use settings::{Settings, SettingsStore, update_settings_file}; use ui::App; use workspace::Workspace; From 85885723a958b2b8af1ee401f26590fb3bf196a5 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:55:19 -0300 Subject: [PATCH 10/46] agent: Fix scrolling in the "Add LLM Provider" modal (#35584) Closes https://github.com/zed-industries/zed/issues/35402 Release Notes: - agent: Fix scrolling in the "Add LLM Provider" modal --- .../add_llm_provider_modal.rs | 72 +++++++++---------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 94b32d156b..401a633488 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -272,42 +272,34 @@ impl AddLlmProviderModal { cx.emit(DismissEvent); } - fn render_section(&self) -> Section { - Section::new() - .child(self.input.provider_name.clone()) - .child(self.input.api_url.clone()) - .child(self.input.api_key.clone()) - } - - fn render_model_section(&self, cx: &mut Context) -> Section { - Section::new().child( - v_flex() - .gap_2() - .child( - h_flex() - .justify_between() - .child(Label::new("Models").size(LabelSize::Small)) - .child( - Button::new("add-model", "Add Model") - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.input.add_model(window, cx); - cx.notify(); - })), - ), - ) - .children( - self.input - .models - .iter() - .enumerate() - .map(|(ix, _)| self.render_model(ix, cx)), - ), - ) + fn render_model_section(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .mt_1() + .gap_2() + .child( + h_flex() + .justify_between() + .child(Label::new("Models").size(LabelSize::Small)) + .child( + Button::new("add-model", "Add Model") + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.input.add_model(window, cx); + cx.notify(); + })), + ), + ) + .children( + self.input + .models + .iter() + .enumerate() + .map(|(ix, _)| self.render_model(ix, cx)), + ) } fn render_model(&self, ix: usize, cx: &mut Context) -> impl IntoElement + use<> { @@ -393,10 +385,14 @@ impl Render for AddLlmProviderModal { .child( v_flex() .id("modal_content") + .size_full() .max_h_128() .overflow_y_scroll() - .gap_2() - .child(self.render_section()) + .px(DynamicSpacing::Base12.rems(cx)) + .gap(DynamicSpacing::Base04.rems(cx)) + .child(self.input.provider_name.clone()) + .child(self.input.api_url.clone()) + .child(self.input.api_key.clone()) .child(self.render_model_section(cx)), ) .footer( From 65018c28c0efb14b4c166141193b3c57dd960259 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 4 Aug 2025 10:22:18 -0600 Subject: [PATCH 11/46] Rename remaining mentions of "inline completion" to "edit prediction" (#35512) Release Notes: - N/A --- Cargo.lock | 96 ++--- Cargo.toml | 10 +- crates/agent_ui/src/inline_assistant.rs | 6 +- crates/agent_ui/src/text_thread_editor.rs | 4 +- crates/collab/src/api/events.rs | 4 +- crates/copilot/Cargo.toml | 2 +- .../src/copilot_completion_provider.rs | 90 ++--- crates/diagnostics/src/diagnostics_tests.rs | 2 +- .../Cargo.toml | 4 +- .../LICENSE-GPL | 0 .../src/edit_prediction.rs} | 14 +- .../Cargo.toml | 6 +- .../LICENSE-GPL | 0 .../src/edit_prediction_button.rs} | 22 +- crates/editor/Cargo.toml | 2 +- crates/editor/src/display_map.rs | 8 +- crates/editor/src/display_map/inlay_map.rs | 26 +- ...tion_tests.rs => edit_prediction_tests.rs} | 67 ++-- crates/editor/src/editor.rs | 343 +++++++++--------- crates/editor/src/editor_tests.rs | 16 +- crates/editor/src/element.rs | 54 +-- crates/editor/src/movement.rs | 4 +- crates/language_tools/src/lsp_log.rs | 4 +- .../src/migrations/m_2025_01_29/keymap.rs | 12 +- crates/rules_library/src/rules_library.rs | 2 +- crates/supermaven/Cargo.toml | 2 +- .../src/supermaven_completion_provider.rs | 8 +- .../telemetry_events/src/telemetry_events.rs | 12 +- crates/vim/src/motion.rs | 4 +- crates/vim/src/normal/change.rs | 4 +- crates/vim/src/normal/delete.rs | 4 +- crates/vim/src/vim.rs | 4 +- crates/zed/Cargo.toml | 2 +- crates/zed/src/main.rs | 12 +- crates/zed/src/zed.rs | 12 +- ...egistry.rs => edit_prediction_registry.rs} | 8 +- crates/zed/src/zed/quick_action_bar.rs | 6 +- crates/zeta/Cargo.toml | 2 +- crates/zeta/src/completion_diff_element.rs | 4 +- crates/zeta/src/rate_completion_modal.rs | 10 +- crates/zeta/src/zeta.rs | 82 ++--- docs/src/ai/llm-providers.md | 2 +- docs/src/key-bindings.md | 2 +- 43 files changed, 480 insertions(+), 498 deletions(-) rename crates/{inline_completion => edit_prediction}/Cargo.toml (82%) rename crates/{inline_completion => edit_prediction}/LICENSE-GPL (100%) rename crates/{inline_completion/src/inline_completion.rs => edit_prediction/src/edit_prediction.rs} (95%) rename crates/{inline_completion_button => edit_prediction_button}/Cargo.toml (90%) rename crates/{inline_completion_button => edit_prediction_button}/LICENSE-GPL (100%) rename crates/{inline_completion_button/src/inline_completion_button.rs => edit_prediction_button/src/edit_prediction_button.rs} (98%) rename crates/editor/src/{inline_completion_tests.rs => edit_prediction_tests.rs} (81%) rename crates/zed/src/zed/{inline_completion_registry.rs => edit_prediction_registry.rs} (96%) diff --git a/Cargo.lock b/Cargo.lock index 7021506502..c21aec93ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3542,13 +3542,13 @@ dependencies = [ "command_palette_hooks", "ctor", "dirs 4.0.0", + "edit_prediction", "editor", "fs", "futures 0.3.31", "gpui", "http_client", "indoc", - "inline_completion", "itertools 0.14.0", "language", "log", @@ -4855,6 +4855,49 @@ dependencies = [ "signature 1.6.4", ] +[[package]] +name = "edit_prediction" +version = "0.1.0" +dependencies = [ + "client", + "gpui", + "language", + "project", + "workspace-hack", +] + +[[package]] +name = "edit_prediction_button" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "cloud_llm_client", + "copilot", + "edit_prediction", + "editor", + "feature_flags", + "fs", + "futures 0.3.31", + "gpui", + "indoc", + "language", + "lsp", + "paths", + "project", + "regex", + "serde_json", + "settings", + "supermaven", + "telemetry", + "theme", + "ui", + "workspace", + "workspace-hack", + "zed_actions", + "zeta", +] + [[package]] name = "editor" version = "0.1.0" @@ -4870,6 +4913,7 @@ dependencies = [ "ctor", "dap", "db", + "edit_prediction", "emojis", "file_icons", "fs", @@ -4879,7 +4923,6 @@ dependencies = [ "gpui", "http_client", "indoc", - "inline_completion", "itertools 0.14.0", "language", "languages", @@ -8287,49 +8330,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "inline_completion" -version = "0.1.0" -dependencies = [ - "client", - "gpui", - "language", - "project", - "workspace-hack", -] - -[[package]] -name = "inline_completion_button" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "cloud_llm_client", - "copilot", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "inline_completion", - "language", - "lsp", - "paths", - "project", - "regex", - "serde_json", - "settings", - "supermaven", - "telemetry", - "theme", - "ui", - "workspace", - "workspace-hack", - "zed_actions", - "zeta", -] - [[package]] name = "inotify" version = "0.9.6" @@ -15584,12 +15584,12 @@ dependencies = [ "anyhow", "client", "collections", + "edit_prediction", "editor", "env_logger 0.11.8", "futures 0.3.31", "gpui", "http_client", - "inline_completion", "language", "log", "postage", @@ -20221,6 +20221,7 @@ dependencies = [ "debugger_tools", "debugger_ui", "diagnostics", + "edit_prediction_button", "editor", "env_logger 0.11.8", "extension", @@ -20240,7 +20241,6 @@ dependencies = [ "http_client", "image_viewer", "indoc", - "inline_completion_button", "inspector_ui", "install_cli", "itertools 0.14.0", @@ -20572,6 +20572,7 @@ dependencies = [ "copilot", "ctor", "db", + "edit_prediction", "editor", "feature_flags", "fs", @@ -20579,7 +20580,6 @@ dependencies = [ "gpui", "http_client", "indoc", - "inline_completion", "language", "language_model", "log", diff --git a/Cargo.toml b/Cargo.toml index 5d852f8842..80796018eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,8 +79,8 @@ members = [ "crates/icons", "crates/image_viewer", "crates/indexed_docs", - "crates/inline_completion", - "crates/inline_completion_button", + "crates/edit_prediction", + "crates/edit_prediction_button", "crates/inspector_ui", "crates/install_cli", "crates/jj", @@ -302,8 +302,8 @@ http_client_tls = { path = "crates/http_client_tls" } icons = { path = "crates/icons" } image_viewer = { path = "crates/image_viewer" } indexed_docs = { path = "crates/indexed_docs" } -inline_completion = { path = "crates/inline_completion" } -inline_completion_button = { path = "crates/inline_completion_button" } +edit_prediction = { path = "crates/edit_prediction" } +edit_prediction_button = { path = "crates/edit_prediction_button" } inspector_ui = { path = "crates/inspector_ui" } install_cli = { path = "crates/install_cli" } jj = { path = "crates/jj" } @@ -756,7 +756,7 @@ feature_flags = { codegen-units = 1 } file_icons = { codegen-units = 1 } fsevent = { codegen-units = 1 } image_viewer = { codegen-units = 1 } -inline_completion_button = { codegen-units = 1 } +edit_prediction_button = { codegen-units = 1 } install_cli = { codegen-units = 1 } journal = { codegen-units = 1 } lmstudio = { codegen-units = 1 } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 159ccd0635..4a4a747899 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -162,7 +162,7 @@ impl InlineAssistant { let window = windows[0]; let _ = window.update(cx, |_, window, cx| { editor.update(cx, |editor, cx| { - if editor.has_active_inline_completion() { + if editor.has_active_edit_prediction() { editor.cancel(&Default::default(), window, cx); } }); @@ -231,8 +231,8 @@ impl InlineAssistant { ); if DisableAiSettings::get_global(cx).disable_ai { - // Cancel any active completions - if editor.has_active_inline_completion() { + // Cancel any active edit predictions + if editor.has_active_edit_prediction() { editor.cancel(&Default::default(), window, cx); } } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 3df0a48aa4..4836a95c8e 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -12,7 +12,7 @@ use assistant_slash_commands::{ use client::{proto, zed_urls}; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use editor::{ - Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot, + Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint, actions::{MoveToEndOfLine, Newline, ShowCompletions}, display_map::{ @@ -254,7 +254,7 @@ impl TextThreadEditor { editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); editor.set_completion_provider(Some(Rc::new(completion_provider))); - editor.set_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never); + editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never); editor.set_collaboration_hub(Box::new(project.clone())); let show_edit_predictions = all_language_settings(None, cx) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index bc7dd152b0..2f34a843a8 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -580,7 +580,7 @@ fn for_snowflake( }, serde_json::to_value(e).unwrap(), ), - Event::InlineCompletion(e) => ( + Event::EditPrediction(e) => ( format!( "Edit Prediction {}", if e.suggestion_accepted { @@ -591,7 +591,7 @@ fn for_snowflake( ), serde_json::to_value(e).unwrap(), ), - Event::InlineCompletionRating(e) => ( + Event::EditPredictionRating(e) => ( "Edit Prediction Rated".to_string(), serde_json::to_value(e).unwrap(), ), diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 234875d420..8908143324 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -34,7 +34,7 @@ fs.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true language.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 8dc04622f9..2a7225c4e3 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1,7 +1,7 @@ use crate::{Completion, Copilot}; use anyhow::Result; +use edit_prediction::{Direction, EditPrediction, EditPredictionProvider}; use gpui::{App, Context, Entity, EntityId, Task}; -use inline_completion::{Direction, EditPredictionProvider, InlineCompletion}; use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings}; use project::Project; use settings::Settings; @@ -210,7 +210,7 @@ impl EditPredictionProvider for CopilotCompletionProvider { buffer: &Entity, cursor_position: language::Anchor, cx: &mut Context, - ) -> Option { + ) -> Option { let buffer_id = buffer.entity_id(); let buffer = buffer.read(cx); let completion = self.active_completion()?; @@ -241,7 +241,7 @@ impl EditPredictionProvider for CopilotCompletionProvider { None } else { let position = cursor_position.bias_right(buffer); - Some(InlineCompletion { + Some(EditPrediction { id: None, edits: vec![(position..position, completion_text.into())], edit_preview: None, @@ -343,7 +343,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { assert!(editor.context_menu_visible()); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); // Since we have both, the copilot suggestion is not shown inline assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n"); @@ -355,7 +355,7 @@ mod tests { .unwrap() .detach(); assert!(!editor.context_menu_visible()); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); }); @@ -389,7 +389,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); // Since only the copilot is available, it's shown inline assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); @@ -400,7 +400,7 @@ mod tests { executor.run_until_parked(); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); @@ -418,25 +418,25 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // Canceling should remove the active Copilot suggestion. editor.cancel(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // After canceling, tabbing shouldn't insert the previously shown suggestion. editor.tab(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. editor.undo(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); @@ -444,25 +444,25 @@ mod tests { // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // AcceptEditPrediction when there is an active suggestion inserts it. editor.accept_edit_prediction(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. editor.undo(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // Hide suggestion. editor.cancel(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); }); @@ -471,7 +471,7 @@ mod tests { // we won't make it visible. cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); cx.update_editor(|editor, _, cx| { - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); }); @@ -498,19 +498,19 @@ mod tests { }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); assert_eq!(editor.text(cx), "fn foo() {\n \n}"); // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. editor.tab(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "fn foo() {\n \n}"); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); // Using AcceptEditPrediction again accepts the suggestion. editor.accept_edit_prediction(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); }); @@ -575,17 +575,17 @@ mod tests { ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); // Accepting the first word of the suggestion should only accept the first word and still show the rest. - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); // Accepting next word should accept the non-word and copilot suggestion should be gone - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); }); @@ -617,11 +617,11 @@ mod tests { ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest. - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n"); assert_eq!( editor.display_text(cx), @@ -629,8 +629,8 @@ mod tests { ); // Accepting next word should accept the next word and copilot suggestion should still exist - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n"); assert_eq!( editor.display_text(cx), @@ -638,8 +638,8 @@ mod tests { ); // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n"); assert_eq!( editor.display_text(cx), @@ -692,29 +692,29 @@ mod tests { }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntw\nthree\n"); editor.backspace(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\nt\nthree\n"); editor.backspace(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\n\nthree\n"); // Deleting across the original suggestion range invalidates it. editor.backspace(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\nthree\n"); assert_eq!(editor.text(cx), "one\nthree\n"); // Undoing the deletion restores the suggestion. editor.undo(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\n\nthree\n"); }); @@ -775,7 +775,7 @@ mod tests { }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); _ = editor.update(cx, |editor, _, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!( editor.display_text(cx), "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n" @@ -797,7 +797,7 @@ mod tests { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) }); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!( editor.display_text(cx), "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n" @@ -806,7 +806,7 @@ mod tests { // Type a character, ensuring we don't even try to interpolate the previous suggestion. editor.handle_input(" ", window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!( editor.display_text(cx), "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n" @@ -817,7 +817,7 @@ mod tests { // Ensure the new suggestion is displayed when the debounce timeout expires. executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); _ = editor.update(cx, |editor, _, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!( editor.display_text(cx), "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n" @@ -880,7 +880,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntw\nthree\n"); }); @@ -907,7 +907,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntwo\nthree\n"); }); @@ -934,7 +934,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(editor.context_menu_visible()); - assert!(!editor.has_active_inline_completion(),); + assert!(!editor.has_active_edit_prediction(),); assert_eq!(editor.text(cx), "one\ntwo.\nthree\n"); }); } @@ -1023,7 +1023,7 @@ mod tests { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) }); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); @@ -1033,7 +1033,7 @@ mod tests { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(5, 0)..Point::new(5, 0)]) }); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 1364aaf853..1bb84488e8 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -873,7 +873,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S editor.splice_inlays( &[], - vec![Inlay::inline_completion( + vec![Inlay::edit_prediction( post_inc(&mut next_inlay_id), snapshot.buffer_snapshot.anchor_before(position), format!("Test inlay {next_inlay_id}"), diff --git a/crates/inline_completion/Cargo.toml b/crates/edit_prediction/Cargo.toml similarity index 82% rename from crates/inline_completion/Cargo.toml rename to crates/edit_prediction/Cargo.toml index 3a90875def..81c1e5dec2 100644 --- a/crates/inline_completion/Cargo.toml +++ b/crates/edit_prediction/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "inline_completion" +name = "edit_prediction" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,7 +9,7 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/inline_completion.rs" +path = "src/edit_prediction.rs" [dependencies] client.workspace = true diff --git a/crates/inline_completion/LICENSE-GPL b/crates/edit_prediction/LICENSE-GPL similarity index 100% rename from crates/inline_completion/LICENSE-GPL rename to crates/edit_prediction/LICENSE-GPL diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/edit_prediction/src/edit_prediction.rs similarity index 95% rename from crates/inline_completion/src/inline_completion.rs rename to crates/edit_prediction/src/edit_prediction.rs index c8f35bf16a..fd4e9bb21d 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -7,7 +7,7 @@ use project::Project; // TODO: Find a better home for `Direction`. // -// This should live in an ancestor crate of `editor` and `inline_completion`, +// This should live in an ancestor crate of `editor` and `edit_prediction`, // but at time of writing there isn't an obvious spot. #[derive(Copy, Clone, PartialEq, Eq)] pub enum Direction { @@ -16,7 +16,7 @@ pub enum Direction { } #[derive(Clone)] -pub struct InlineCompletion { +pub struct EditPrediction { /// The ID of the completion, if it has one. pub id: Option, pub edits: Vec<(Range, String)>, @@ -102,10 +102,10 @@ pub trait EditPredictionProvider: 'static + Sized { buffer: &Entity, cursor_position: language::Anchor, cx: &mut Context, - ) -> Option; + ) -> Option; } -pub trait InlineCompletionProviderHandle { +pub trait EditPredictionProviderHandle { fn name(&self) -> &'static str; fn display_name(&self) -> &'static str; fn is_enabled( @@ -143,10 +143,10 @@ pub trait InlineCompletionProviderHandle { buffer: &Entity, cursor_position: language::Anchor, cx: &mut App, - ) -> Option; + ) -> Option; } -impl InlineCompletionProviderHandle for Entity +impl EditPredictionProviderHandle for Entity where T: EditPredictionProvider, { @@ -233,7 +233,7 @@ where buffer: &Entity, cursor_position: language::Anchor, cx: &mut App, - ) -> Option { + ) -> Option { self.update(cx, |this, cx| this.suggest(buffer, cursor_position, cx)) } } diff --git a/crates/inline_completion_button/Cargo.toml b/crates/edit_prediction_button/Cargo.toml similarity index 90% rename from crates/inline_completion_button/Cargo.toml rename to crates/edit_prediction_button/Cargo.toml index 7b6ae43465..07447280fa 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/edit_prediction_button/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "inline_completion_button" +name = "edit_prediction_button" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,7 +9,7 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/inline_completion_button.rs" +path = "src/edit_prediction_button.rs" doctest = false [dependencies] @@ -22,7 +22,7 @@ feature_flags.workspace = true fs.workspace = true gpui.workspace = true indoc.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true language.workspace = true paths.workspace = true project.workspace = true diff --git a/crates/inline_completion_button/LICENSE-GPL b/crates/edit_prediction_button/LICENSE-GPL similarity index 100% rename from crates/inline_completion_button/LICENSE-GPL rename to crates/edit_prediction_button/LICENSE-GPL diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs similarity index 98% rename from crates/inline_completion_button/src/inline_completion_button.rs rename to crates/edit_prediction_button/src/edit_prediction_button.rs index 79ebc573df..33165bccf8 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -41,7 +41,7 @@ use zeta::RateCompletions; actions!( edit_prediction, [ - /// Toggles the inline completion menu. + /// Toggles the edit prediction menu. ToggleMenu ] ); @@ -51,14 +51,14 @@ const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security"; struct CopilotErrorToast; -pub struct InlineCompletionButton { +pub struct EditPredictionButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, editor_show_predictions: bool, editor_focus_handle: Option, language: Option>, file: Option>, - edit_prediction_provider: Option>, + edit_prediction_provider: Option>, fs: Arc, user_store: Entity, popover_menu_handle: PopoverMenuHandle, @@ -71,7 +71,7 @@ enum SupermavenButtonStatus { Initializing, } -impl Render for InlineCompletionButton { +impl Render for EditPredictionButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { // Return empty div if AI is disabled if DisableAiSettings::get_global(cx).disable_ai { @@ -369,7 +369,7 @@ impl Render for InlineCompletionButton { } } -impl InlineCompletionButton { +impl EditPredictionButton { pub fn new( fs: Arc, user_store: Entity, @@ -470,7 +470,7 @@ impl InlineCompletionButton { IconPosition::Start, None, move |_, cx| { - toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx) + toggle_show_edit_predictions_for_language(language.clone(), fs.clone(), cx) }, ); } @@ -480,7 +480,7 @@ impl InlineCompletionButton { let globally_enabled = settings.show_edit_predictions(None, cx); menu = menu.toggleable_entry("All Files", globally_enabled, IconPosition::Start, None, { let fs = fs.clone(); - move |_, cx| toggle_inline_completions_globally(fs.clone(), cx) + move |_, cx| toggle_edit_predictions_globally(fs.clone(), cx) }); let provider = settings.edit_predictions.provider; @@ -835,7 +835,7 @@ impl InlineCompletionButton { } } -impl StatusItemView for InlineCompletionButton { +impl StatusItemView for EditPredictionButton { fn set_active_pane_item( &mut self, item: Option<&dyn ItemHandle>, @@ -905,7 +905,7 @@ async fn open_disabled_globs_setting_in_editor( let settings = cx.global::(); - // Ensure that we always have "inline_completions { "disabled_globs": [] }" + // Ensure that we always have "edit_predictions { "disabled_globs": [] }" let edits = settings.edits_for_update::(&text, |file| { file.edit_predictions .get_or_insert_with(Default::default) @@ -943,7 +943,7 @@ async fn open_disabled_globs_setting_in_editor( anyhow::Ok(()) } -fn toggle_inline_completions_globally(fs: Arc, cx: &mut App) { +fn toggle_edit_predictions_globally(fs: Arc, cx: &mut App) { let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); update_settings_file::(fs, cx, move |file, _| { file.defaults.show_edit_predictions = Some(!show_edit_predictions) @@ -958,7 +958,7 @@ fn set_completion_provider(fs: Arc, cx: &mut App, provider: EditPredicti }); } -fn toggle_show_inline_completions_for_language( +fn toggle_show_edit_predictions_for_language( language: Arc, fs: Arc, cx: &mut App, diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index ab2d1c8ecb..339f98ae8b 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -48,7 +48,7 @@ fs.workspace = true git.workspace = true gpui.workspace = true indoc.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true itertools.workspace = true language.workspace = true linkify.workspace = true diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 5425d5a8b9..a16e516a70 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -635,7 +635,7 @@ pub(crate) struct Highlights<'a> { } #[derive(Clone, Copy, Debug)] -pub struct InlineCompletionStyles { +pub struct EditPredictionStyles { pub insertion: HighlightStyle, pub whitespace: HighlightStyle, } @@ -643,7 +643,7 @@ pub struct InlineCompletionStyles { #[derive(Default, Debug, Clone, Copy)] pub struct HighlightStyles { pub inlay_hint: Option, - pub inline_completion: Option, + pub edit_prediction: Option, } #[derive(Clone)] @@ -958,7 +958,7 @@ impl DisplaySnapshot { language_aware, HighlightStyles { inlay_hint: Some(editor_style.inlay_hints_style), - inline_completion: Some(editor_style.inline_completion_styles), + edit_prediction: Some(editor_style.edit_prediction_styles), }, ) .flat_map(|chunk| { @@ -2036,7 +2036,7 @@ pub mod tests { map.update(cx, |map, cx| { map.splice_inlays( &[], - vec![Inlay::inline_completion( + vec![Inlay::edit_prediction( 0, buffer_snapshot.anchor_after(0), "\n", diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index a36d18ff6d..0b1c7a4bed 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -81,9 +81,9 @@ impl Inlay { } } - pub fn inline_completion>(id: usize, position: Anchor, text: T) -> Self { + pub fn edit_prediction>(id: usize, position: Anchor, text: T) -> Self { Self { - id: InlayId::InlineCompletion(id), + id: InlayId::EditPrediction(id), position, text: text.into(), color: None, @@ -340,15 +340,13 @@ impl<'a> Iterator for InlayChunks<'a> { let mut renderer = None; let mut highlight_style = match inlay.id { - InlayId::InlineCompletion(_) => { - self.highlight_styles.inline_completion.map(|s| { - if inlay.text.chars().all(|c| c.is_whitespace()) { - s.whitespace - } else { - s.insertion - } - }) - } + InlayId::EditPrediction(_) => self.highlight_styles.edit_prediction.map(|s| { + if inlay.text.chars().all(|c| c.is_whitespace()) { + s.whitespace + } else { + s.insertion + } + }), InlayId::Hint(_) => self.highlight_styles.inlay_hint, InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint, InlayId::Color(_) => { @@ -740,7 +738,7 @@ impl InlayMap { text.clone(), ) } else { - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(next_inlay_id), snapshot.buffer.anchor_at(position, bias), text.clone(), @@ -1389,7 +1387,7 @@ mod tests { buffer.read(cx).snapshot(cx).anchor_before(3), "|123|", ), - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(&mut next_inlay_id), buffer.read(cx).snapshot(cx).anchor_after(3), "|456|", @@ -1609,7 +1607,7 @@ mod tests { buffer.read(cx).snapshot(cx).anchor_before(4), "|456|", ), - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(&mut next_inlay_id), buffer.read(cx).snapshot(cx).anchor_before(7), "\n|567|\n", diff --git a/crates/editor/src/inline_completion_tests.rs b/crates/editor/src/edit_prediction_tests.rs similarity index 81% rename from crates/editor/src/inline_completion_tests.rs rename to crates/editor/src/edit_prediction_tests.rs index 5ac34c94f5..527dfb8832 100644 --- a/crates/editor/src/inline_completion_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -1,26 +1,26 @@ +use edit_prediction::EditPredictionProvider; use gpui::{Entity, prelude::*}; use indoc::indoc; -use inline_completion::EditPredictionProvider; use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint}; use project::Project; use std::ops::Range; use text::{Point, ToOffset}; use crate::{ - InlineCompletion, editor_tests::init_test, test::editor_test_context::EditorTestContext, + EditPrediction, editor_tests::init_test, test::editor_test_context::EditorTestContext, }; #[gpui::test] -async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let absolute_zero_celsius = ˇ;"); propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); @@ -33,16 +33,16 @@ async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_modification(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let pi = ˇ\"foo\";"); propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); @@ -55,11 +55,11 @@ async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 2+ lines above the proposed edit @@ -77,7 +77,7 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3)); }); @@ -107,7 +107,7 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3)); }); @@ -124,11 +124,11 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 3+ lines above the proposed edit @@ -148,7 +148,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); @@ -176,7 +176,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext line "}); cx.editor(|editor, _, _| { - assert!(editor.active_inline_completion.is_none()); + assert!(editor.active_edit_prediction.is_none()); }); // Cursor is 3+ lines below the proposed edit @@ -196,7 +196,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); @@ -224,7 +224,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext line ˇ5 "}); cx.editor(|editor, _, _| { - assert!(editor.active_inline_completion.is_none()); + assert!(editor.active_edit_prediction.is_none()); }); } @@ -234,11 +234,11 @@ fn assert_editor_active_edit_completion( ) { cx.editor(|editor, _, cx| { let completion_state = editor - .active_inline_completion + .active_edit_prediction .as_ref() .expect("editor has no active completion"); - if let InlineCompletion::Edit { edits, .. } = &completion_state.completion { + if let EditPrediction::Edit { edits, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), edits); } else { panic!("expected edit completion"); @@ -252,11 +252,11 @@ fn assert_editor_active_move_completion( ) { cx.editor(|editor, _, cx| { let completion_state = editor - .active_inline_completion + .active_edit_prediction .as_ref() .expect("editor has no active completion"); - if let InlineCompletion::Move { target, .. } = &completion_state.completion { + if let EditPrediction::Move { target, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), *target); } else { panic!("expected move completion"); @@ -271,7 +271,7 @@ fn accept_completion(cx: &mut EditorTestContext) { } fn propose_edits( - provider: &Entity, + provider: &Entity, edits: Vec<(Range, &str)>, cx: &mut EditorTestContext, ) { @@ -283,7 +283,7 @@ fn propose_edits( cx.update(|_, cx| { provider.update(cx, |provider, _| { - provider.set_inline_completion(Some(inline_completion::InlineCompletion { + provider.set_edit_prediction(Some(edit_prediction::EditPrediction { id: None, edits: edits.collect(), edit_preview: None, @@ -293,7 +293,7 @@ fn propose_edits( } fn assign_editor_completion_provider( - provider: Entity, + provider: Entity, cx: &mut EditorTestContext, ) { cx.update_editor(|editor, window, cx| { @@ -302,20 +302,17 @@ fn assign_editor_completion_provider( } #[derive(Default, Clone)] -pub struct FakeInlineCompletionProvider { - pub completion: Option, +pub struct FakeEditPredictionProvider { + pub completion: Option, } -impl FakeInlineCompletionProvider { - pub fn set_inline_completion( - &mut self, - completion: Option, - ) { +impl FakeEditPredictionProvider { + pub fn set_edit_prediction(&mut self, completion: Option) { self.completion = completion; } } -impl EditPredictionProvider for FakeInlineCompletionProvider { +impl EditPredictionProvider for FakeEditPredictionProvider { fn name() -> &'static str { "fake-completion-provider" } @@ -355,7 +352,7 @@ impl EditPredictionProvider for FakeInlineCompletionProvider { &mut self, _buffer: gpui::Entity, _cursor_position: language::Anchor, - _direction: inline_completion::Direction, + _direction: edit_prediction::Direction, _cx: &mut gpui::Context, ) { } @@ -369,7 +366,7 @@ impl EditPredictionProvider for FakeInlineCompletionProvider { _buffer: &gpui::Entity, _cursor_position: language::Anchor, _cx: &mut gpui::Context, - ) -> Option { + ) -> Option { self.completion.clone() } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8e1efc0701..2912708b56 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -43,15 +43,16 @@ pub mod tasks; #[cfg(test)] mod code_completion_tests; #[cfg(test)] -mod editor_tests; +mod edit_prediction_tests; #[cfg(test)] -mod inline_completion_tests; +mod editor_tests; mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; pub(crate) use actions::*; pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; +pub use edit_prediction::Direction; pub use editor_settings::{ CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, @@ -62,7 +63,6 @@ pub use element::{ }; pub use git::blame::BlameRenderer; pub use hover_popover::hover_markdown_style; -pub use inline_completion::Direction; pub use items::MAX_TAB_TITLE_LEN; pub use lsp::CompletionContext; pub use lsp_ext::lsp_tasks; @@ -93,6 +93,7 @@ use collections::{BTreeMap, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use dap::TelemetrySpawnLocation; use display_map::*; +use edit_prediction::{EditPredictionProvider, EditPredictionProviderHandle}; use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings}; use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; use futures::{ @@ -117,7 +118,6 @@ use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; use hover_popover::{HoverState, hide_hover}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; -use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; use itertools::Itertools; use language::{ AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow, @@ -268,7 +268,7 @@ impl InlineValueCache { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum InlayId { - InlineCompletion(usize), + EditPrediction(usize), DebuggerValue(usize), // LSP Hint(usize), @@ -278,7 +278,7 @@ pub enum InlayId { impl InlayId { fn id(&self) -> usize { match self { - Self::InlineCompletion(id) => *id, + Self::EditPrediction(id) => *id, Self::DebuggerValue(id) => *id, Self::Hint(id) => *id, Self::Color(id) => *id, @@ -547,7 +547,7 @@ pub struct EditorStyle { pub syntax: Arc, pub status: StatusColors, pub inlay_hints_style: HighlightStyle, - pub inline_completion_styles: InlineCompletionStyles, + pub edit_prediction_styles: EditPredictionStyles, pub unnecessary_code_fade: f32, pub show_underlines: bool, } @@ -566,7 +566,7 @@ impl Default for EditorStyle { // style and retrieve them directly from the theme. status: StatusColors::dark(), inlay_hints_style: HighlightStyle::default(), - inline_completion_styles: InlineCompletionStyles { + edit_prediction_styles: EditPredictionStyles { insertion: HighlightStyle::default(), whitespace: HighlightStyle::default(), }, @@ -588,8 +588,8 @@ pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { } } -pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles { - InlineCompletionStyles { +pub fn make_suggestion_styles(cx: &mut App) -> EditPredictionStyles { + EditPredictionStyles { insertion: HighlightStyle { color: Some(cx.theme().status().predictive), ..HighlightStyle::default() @@ -609,7 +609,7 @@ pub(crate) enum EditDisplayMode { Inline, } -enum InlineCompletion { +enum EditPrediction { Edit { edits: Vec<(Range, String)>, edit_preview: Option, @@ -622,9 +622,9 @@ enum InlineCompletion { }, } -struct InlineCompletionState { +struct EditPredictionState { inlay_ids: Vec, - completion: InlineCompletion, + completion: EditPrediction, completion_id: Option, invalidation_range: Range, } @@ -637,7 +637,7 @@ enum EditPredictionSettings { }, } -enum InlineCompletionHighlight {} +enum EditPredictionHighlight {} #[derive(Debug, Clone)] struct InlineDiagnostic { @@ -648,7 +648,7 @@ struct InlineDiagnostic { severity: lsp::DiagnosticSeverity, } -pub enum MenuInlineCompletionsPolicy { +pub enum MenuEditPredictionsPolicy { Never, ByProvider, } @@ -1087,15 +1087,15 @@ pub struct Editor { pending_mouse_down: Option>>>, gutter_hovered: bool, hovered_link_state: Option, - edit_prediction_provider: Option, + edit_prediction_provider: Option, code_action_providers: Vec>, - active_inline_completion: Option, + active_edit_prediction: Option, /// Used to prevent flickering as the user types while the menu is open - stale_inline_completion_in_menu: Option, + stale_edit_prediction_in_menu: Option, edit_prediction_settings: EditPredictionSettings, - inline_completions_hidden_for_vim_mode: bool, - show_inline_completions_override: Option, - menu_inline_completions_policy: MenuInlineCompletionsPolicy, + edit_predictions_hidden_for_vim_mode: bool, + show_edit_predictions_override: Option, + menu_edit_predictions_policy: MenuEditPredictionsPolicy, edit_prediction_preview: EditPredictionPreview, edit_prediction_indent_conflict: bool, edit_prediction_requires_modifier_in_indent_conflict: bool, @@ -1510,8 +1510,8 @@ pub struct RenameState { struct InvalidationStack(Vec); -struct RegisteredInlineCompletionProvider { - provider: Arc, +struct RegisteredEditPredictionProvider { + provider: Arc, _subscription: Subscription, } @@ -2096,8 +2096,8 @@ impl Editor { pending_mouse_down: None, hovered_link_state: None, edit_prediction_provider: None, - active_inline_completion: None, - stale_inline_completion_in_menu: None, + active_edit_prediction: None, + stale_edit_prediction_in_menu: None, edit_prediction_preview: EditPredictionPreview::Inactive { released_too_fast: false, }, @@ -2116,9 +2116,9 @@ impl Editor { hovered_cursors: HashMap::default(), next_editor_action_id: EditorActionId::default(), editor_actions: Rc::default(), - inline_completions_hidden_for_vim_mode: false, - show_inline_completions_override: None, - menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, + edit_predictions_hidden_for_vim_mode: false, + show_edit_predictions_override: None, + menu_edit_predictions_policy: MenuEditPredictionsPolicy::ByProvider, edit_prediction_settings: EditPredictionSettings::Disabled, edit_prediction_indent_conflict: false, edit_prediction_requires_modifier_in_indent_conflict: true, @@ -2350,7 +2350,7 @@ impl Editor { } pub fn key_context(&self, window: &Window, cx: &App) -> KeyContext { - self.key_context_internal(self.has_active_inline_completion(), window, cx) + self.key_context_internal(self.has_active_edit_prediction(), window, cx) } fn key_context_internal( @@ -2717,17 +2717,16 @@ impl Editor { ) where T: EditPredictionProvider, { - self.edit_prediction_provider = - provider.map(|provider| RegisteredInlineCompletionProvider { - _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { - if this.focus_handle.is_focused(window) { - this.update_visible_inline_completion(window, cx); - } - }), - provider: Arc::new(provider), - }); + self.edit_prediction_provider = provider.map(|provider| RegisteredEditPredictionProvider { + _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { + if this.focus_handle.is_focused(window) { + this.update_visible_edit_prediction(window, cx); + } + }), + provider: Arc::new(provider), + }); self.update_edit_prediction_settings(cx); - self.refresh_inline_completion(false, false, window, cx); + self.refresh_edit_prediction(false, false, window, cx); } pub fn placeholder_text(&self) -> Option<&str> { @@ -2798,24 +2797,24 @@ impl Editor { self.input_enabled = input_enabled; } - pub fn set_inline_completions_hidden_for_vim_mode( + pub fn set_edit_predictions_hidden_for_vim_mode( &mut self, hidden: bool, window: &mut Window, cx: &mut Context, ) { - if hidden != self.inline_completions_hidden_for_vim_mode { - self.inline_completions_hidden_for_vim_mode = hidden; + if hidden != self.edit_predictions_hidden_for_vim_mode { + self.edit_predictions_hidden_for_vim_mode = hidden; if hidden { - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); } else { - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); } } } - pub fn set_menu_inline_completions_policy(&mut self, value: MenuInlineCompletionsPolicy) { - self.menu_inline_completions_policy = value; + pub fn set_menu_edit_predictions_policy(&mut self, value: MenuEditPredictionsPolicy) { + self.menu_edit_predictions_policy = value; } pub fn set_autoindent(&mut self, autoindent: bool) { @@ -2852,7 +2851,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.show_inline_completions_override.is_some() { + if self.show_edit_predictions_override.is_some() { self.set_show_edit_predictions(None, window, cx); } else { let show_edit_predictions = !self.edit_predictions_enabled(); @@ -2866,17 +2865,17 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.show_inline_completions_override = show_edit_predictions; + self.show_edit_predictions_override = show_edit_predictions; self.update_edit_prediction_settings(cx); if let Some(false) = show_edit_predictions { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); } else { - self.refresh_inline_completion(false, true, window, cx); + self.refresh_edit_prediction(false, true, window, cx); } } - fn inline_completions_disabled_in_scope( + fn edit_predictions_disabled_in_scope( &self, buffer: &Entity, buffer_position: language::Anchor, @@ -3043,7 +3042,7 @@ impl Editor { self.refresh_document_highlights(cx); self.refresh_selected_text_highlights(false, window, cx); refresh_matching_bracket_highlights(self, window, cx); - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); self.edit_prediction_requires_modifier_in_indent_conflict = true; linked_editing_ranges::refresh_linked_ranges(self, window, cx); self.inline_blame_popover.take(); @@ -3833,7 +3832,7 @@ impl Editor { return true; } - if is_user_requested && self.discard_inline_completion(true, cx) { + if is_user_requested && self.discard_edit_prediction(true, cx) { return true; } @@ -4239,7 +4238,7 @@ impl Editor { ); } - let had_active_inline_completion = this.has_active_inline_completion(); + let had_active_edit_prediction = this.has_active_edit_prediction(); this.change_selections( SelectionEffects::scroll(Autoscroll::fit()).completions(false), window, @@ -4264,7 +4263,7 @@ impl Editor { } let trigger_in_words = - this.show_edit_predictions_in_menu() || !had_active_inline_completion; + this.show_edit_predictions_in_menu() || !had_active_edit_prediction; if this.hard_wrap.is_some() { let latest: Range = this.selections.newest(cx).range(); if latest.is_empty() @@ -4286,7 +4285,7 @@ impl Editor { } this.trigger_completion_on_input(&text, trigger_in_words, window, cx); linked_editing_ranges::refresh_linked_ranges(this, window, cx); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); }); } @@ -4621,7 +4620,7 @@ impl Editor { .collect(); this.change_selections(Default::default(), window, cx, |s| s.select(new_selections)); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -5669,9 +5668,9 @@ impl Editor { crate::hover_popover::hide_hover(editor, cx); if editor.show_edit_predictions_in_menu() { - editor.update_visible_inline_completion(window, cx); + editor.update_visible_edit_prediction(window, cx); } else { - editor.discard_inline_completion(false, cx); + editor.discard_edit_prediction(false, cx); } cx.notify(); @@ -5682,10 +5681,10 @@ impl Editor { if editor.completion_tasks.len() <= 1 { // If there are no more completion tasks and the last menu was empty, we should hide it. let was_hidden = editor.hide_context_menu(window, cx).is_none(); - // If it was already hidden and we don't show inline completions in the menu, we should - // also show the inline-completion when available. + // If it was already hidden and we don't show edit predictions in the menu, + // we should also show the edit prediction when available. if was_hidden && editor.show_edit_predictions_in_menu() { - editor.update_visible_inline_completion(window, cx); + editor.update_visible_edit_prediction(window, cx); } } }) @@ -5779,7 +5778,7 @@ impl Editor { let entries = completions_menu.entries.borrow(); let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; if self.show_edit_predictions_in_menu() { - self.discard_inline_completion(true, cx); + self.discard_edit_prediction(true, cx); } mat.candidate_id }; @@ -5923,7 +5922,7 @@ impl Editor { }) } - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot); @@ -5983,7 +5982,7 @@ impl Editor { let deployed_from = action.deployed_from.clone(); let action = action.clone(); self.completion_tasks.clear(); - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); let multibuffer_point = match &action.deployed_from { Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { @@ -6988,7 +6987,7 @@ impl Editor { } } - pub fn refresh_inline_completion( + pub fn refresh_edit_prediction( &mut self, debounce: bool, user_requested: bool, @@ -7005,7 +7004,7 @@ impl Editor { self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); return None; } @@ -7014,11 +7013,11 @@ impl Editor { || !self.is_focused(window) || buffer.read(cx).is_empty()) { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); return None; } - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); provider.refresh( self.project.clone(), buffer, @@ -7056,7 +7055,7 @@ impl Editor { pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai { self.edit_prediction_settings = EditPredictionSettings::Disabled; - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); } else { let selection = self.selections.newest_anchor(); let cursor = selection.head(); @@ -7077,8 +7076,8 @@ impl Editor { cx: &App, ) -> EditPredictionSettings { if !self.mode.is_full() - || !self.show_inline_completions_override.unwrap_or(true) - || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) + || !self.show_edit_predictions_override.unwrap_or(true) + || self.edit_predictions_disabled_in_scope(buffer, buffer_position, cx) { return EditPredictionSettings::Disabled; } @@ -7092,8 +7091,8 @@ impl Editor { }; let by_provider = matches!( - self.menu_inline_completions_policy, - MenuInlineCompletionsPolicy::ByProvider + self.menu_edit_predictions_policy, + MenuEditPredictionsPolicy::ByProvider ); let show_in_menu = by_provider @@ -7163,7 +7162,7 @@ impl Editor { .unwrap_or(false) } - fn cycle_inline_completion( + fn cycle_edit_prediction( &mut self, direction: Direction, window: &mut Window, @@ -7173,28 +7172,28 @@ impl Editor { let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() { + if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() { return None; } provider.cycle(buffer, cursor_buffer_position, direction, cx); - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); Some(()) } - pub fn show_inline_completion( + pub fn show_edit_prediction( &mut self, _: &ShowEditPrediction, window: &mut Window, cx: &mut Context, ) { - if !self.has_active_inline_completion() { - self.refresh_inline_completion(false, true, window, cx); + if !self.has_active_edit_prediction() { + self.refresh_edit_prediction(false, true, window, cx); return; } - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); } pub fn display_cursor_names( @@ -7226,11 +7225,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Next, window, cx); + if self.has_active_edit_prediction() { + self.cycle_edit_prediction(Direction::Next, window, cx); } else { let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) + .refresh_edit_prediction(false, true, window, cx) .is_none(); if is_copilot_disabled { cx.propagate(); @@ -7244,11 +7243,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Prev, window, cx); + if self.has_active_edit_prediction() { + self.cycle_edit_prediction(Direction::Prev, window, cx); } else { let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) + .refresh_edit_prediction(false, true, window, cx) .is_none(); if is_copilot_disabled { cx.propagate(); @@ -7266,18 +7265,14 @@ impl Editor { self.hide_context_menu(window, cx); } - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else { return; }; - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); + self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx); - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { + match &active_edit_prediction.completion { + EditPrediction::Move { target, .. } => { let target = *target; if let Some(position_map) = &self.last_position_map { @@ -7319,7 +7314,7 @@ impl Editor { } } } - InlineCompletion::Edit { edits, .. } => { + EditPrediction::Edit { edits, .. } => { if let Some(provider) = self.edit_prediction_provider() { provider.accept(cx); } @@ -7347,9 +7342,9 @@ impl Editor { } } - self.update_visible_inline_completion(window, cx); - if self.active_inline_completion.is_none() { - self.refresh_inline_completion(true, true, window, cx); + self.update_visible_edit_prediction(window, cx); + if self.active_edit_prediction.is_none() { + self.refresh_edit_prediction(true, true, window, cx); } cx.notify(); @@ -7359,27 +7354,23 @@ impl Editor { self.edit_prediction_requires_modifier_in_indent_conflict = false; } - pub fn accept_partial_inline_completion( + pub fn accept_partial_edit_prediction( &mut self, _: &AcceptPartialEditPrediction, window: &mut Window, cx: &mut Context, ) { - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else { return; }; if self.selections.count() != 1 { return; } - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); + self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx); - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { + match &active_edit_prediction.completion { + EditPrediction::Move { target, .. } => { let target = *target; self.change_selections( SelectionEffects::scroll(Autoscroll::newest()), @@ -7390,7 +7381,7 @@ impl Editor { }, ); } - InlineCompletion::Edit { edits, .. } => { + EditPrediction::Edit { edits, .. } => { // Find an insertion that starts at the cursor position. let snapshot = self.buffer.read(cx).snapshot(cx); let cursor_offset = self.selections.newest::(cx).head(); @@ -7424,7 +7415,7 @@ impl Editor { self.insert_with_autoindent_mode(&partial_completion, None, window, cx); - self.refresh_inline_completion(true, true, window, cx); + self.refresh_edit_prediction(true, true, window, cx); cx.notify(); } else { self.accept_edit_prediction(&Default::default(), window, cx); @@ -7433,28 +7424,28 @@ impl Editor { } } - fn discard_inline_completion( + fn discard_edit_prediction( &mut self, - should_report_inline_completion_event: bool, + should_report_edit_prediction_event: bool, cx: &mut Context, ) -> bool { - if should_report_inline_completion_event { + if should_report_edit_prediction_event { let completion_id = self - .active_inline_completion + .active_edit_prediction .as_ref() .and_then(|active_completion| active_completion.completion_id.clone()); - self.report_inline_completion_event(completion_id, false, cx); + self.report_edit_prediction_event(completion_id, false, cx); } if let Some(provider) = self.edit_prediction_provider() { provider.discard(cx); } - self.take_active_inline_completion(cx) + self.take_active_edit_prediction(cx) } - fn report_inline_completion_event(&self, id: Option, accepted: bool, cx: &App) { + fn report_edit_prediction_event(&self, id: Option, accepted: bool, cx: &App) { let Some(provider) = self.edit_prediction_provider() else { return; }; @@ -7485,18 +7476,18 @@ impl Editor { ); } - pub fn has_active_inline_completion(&self) -> bool { - self.active_inline_completion.is_some() + pub fn has_active_edit_prediction(&self) -> bool { + self.active_edit_prediction.is_some() } - fn take_active_inline_completion(&mut self, cx: &mut Context) -> bool { - let Some(active_inline_completion) = self.active_inline_completion.take() else { + fn take_active_edit_prediction(&mut self, cx: &mut Context) -> bool { + let Some(active_edit_prediction) = self.active_edit_prediction.take() else { return false; }; - self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx); - self.clear_highlights::(cx); - self.stale_inline_completion_in_menu = Some(active_inline_completion); + self.splice_inlays(&active_edit_prediction.inlay_ids, Default::default(), cx); + self.clear_highlights::(cx); + self.stale_edit_prediction_in_menu = Some(active_edit_prediction); true } @@ -7641,7 +7632,7 @@ impl Editor { since: Instant::now(), }; - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); cx.notify(); } } else if let EditPredictionPreview::Active { @@ -7664,12 +7655,12 @@ impl Editor { released_too_fast: since.elapsed() < Duration::from_millis(200), }; self.clear_row_highlights::(); - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); cx.notify(); } } - fn update_visible_inline_completion( + fn update_visible_edit_prediction( &mut self, _window: &mut Window, cx: &mut Context, @@ -7687,12 +7678,12 @@ impl Editor { let show_in_menu = self.show_edit_predictions_in_menu(); let completions_menu_has_precedence = !show_in_menu && (self.context_menu.borrow().is_some() - || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())); + || (!self.completion_tasks.is_empty() && !self.has_active_edit_prediction())); if completions_menu_has_precedence || !offset_selection.is_empty() || self - .active_inline_completion + .active_edit_prediction .as_ref() .map_or(false, |completion| { let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); @@ -7700,11 +7691,11 @@ impl Editor { !invalidation_range.contains(&offset_selection.head()) }) { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); return None; } - self.take_active_inline_completion(cx); + self.take_active_edit_prediction(cx); let Some(provider) = self.edit_prediction_provider() else { self.edit_prediction_settings = EditPredictionSettings::Disabled; return None; @@ -7730,8 +7721,8 @@ impl Editor { } } - let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; - let edits = inline_completion + let edit_prediction = provider.suggest(&buffer, cursor_buffer_position, cx)?; + let edits = edit_prediction .edits .into_iter() .flat_map(|(range, new_text)| { @@ -7766,15 +7757,15 @@ impl Editor { None }; let is_move = - move_invalidation_row_range.is_some() || self.inline_completions_hidden_for_vim_mode; + move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode; let completion = if is_move { invalidation_row_range = move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); let target = first_edit_start; - InlineCompletion::Move { target, snapshot } + EditPrediction::Move { target, snapshot } } else { let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true) - && !self.inline_completions_hidden_for_vim_mode; + && !self.edit_predictions_hidden_for_vim_mode; if show_completions_in_buffer { if edits @@ -7783,7 +7774,7 @@ impl Editor { { let mut inlays = Vec::new(); for (range, new_text) in &edits { - let inlay = Inlay::inline_completion( + let inlay = Inlay::edit_prediction( post_inc(&mut self.next_inlay_id), range.start, new_text.as_str(), @@ -7795,7 +7786,7 @@ impl Editor { self.splice_inlays(&[], inlays, cx); } else { let background_color = cx.theme().status().deleted_background; - self.highlight_text::( + self.highlight_text::( edits.iter().map(|(range, _)| range.clone()).collect(), HighlightStyle { background_color: Some(background_color), @@ -7818,9 +7809,9 @@ impl Editor { EditDisplayMode::DiffPopover }; - InlineCompletion::Edit { + EditPrediction::Edit { edits, - edit_preview: inline_completion.edit_preview, + edit_preview: edit_prediction.edit_preview, display_mode, snapshot, } @@ -7833,11 +7824,11 @@ impl Editor { multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)), )); - self.stale_inline_completion_in_menu = None; - self.active_inline_completion = Some(InlineCompletionState { + self.stale_edit_prediction_in_menu = None; + self.active_edit_prediction = Some(EditPredictionState { inlay_ids, completion, - completion_id: inline_completion.id, + completion_id: edit_prediction.id, invalidation_range, }); @@ -7846,7 +7837,7 @@ impl Editor { Some(()) } - pub fn edit_prediction_provider(&self) -> Option> { + pub fn edit_prediction_provider(&self) -> Option> { Some(self.edit_prediction_provider.as_ref()?.provider.clone()) } @@ -8415,14 +8406,14 @@ impl Editor { if self.mode().is_minimap() { return None; } - let active_inline_completion = self.active_inline_completion.as_ref()?; + let active_edit_prediction = self.active_edit_prediction.as_ref()?; if self.edit_prediction_visible_in_cursor_popover(true) { return None; } - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { + match &active_edit_prediction.completion { + EditPrediction::Move { target, .. } => { let target_display_point = target.to_display_point(editor_snapshot); if self.edit_prediction_requires_modifier() { @@ -8459,11 +8450,11 @@ impl Editor { ) } } - InlineCompletion::Edit { + EditPrediction::Edit { display_mode: EditDisplayMode::Inline, .. } => None, - InlineCompletion::Edit { + EditPrediction::Edit { display_mode: EditDisplayMode::TabAccept, edits, .. @@ -8484,7 +8475,7 @@ impl Editor { cx, ) } - InlineCompletion::Edit { + EditPrediction::Edit { edits, edit_preview, display_mode: EditDisplayMode::DiffPopover, @@ -8801,7 +8792,7 @@ impl Editor { } let highlighted_edits = - crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); + crate::edit_prediction_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); let styled_text = highlighted_edits.to_styled_text(&style.text); let line_count = highlighted_edits.text.lines().count(); @@ -9131,7 +9122,7 @@ impl Editor { .child(Icon::new(IconName::ZedPredict)) } - let completion = match &self.active_inline_completion { + let completion = match &self.active_edit_prediction { Some(prediction) => { if !self.has_visible_completions_menu() { const RADIUS: Pixels = px(6.); @@ -9149,7 +9140,7 @@ impl Editor { .rounded_tl(px(0.)) .overflow_hidden() .child(div().px_1p5().child(match &prediction.completion { - InlineCompletion::Move { target, snapshot } => { + EditPrediction::Move { target, snapshot } => { use text::ToPoint as _; if target.text_anchor.to_point(&snapshot).row > cursor_point.row { @@ -9158,7 +9149,7 @@ impl Editor { Icon::new(IconName::ZedPredictUp) } } - InlineCompletion::Edit { .. } => Icon::new(IconName::ZedPredict), + EditPrediction::Edit { .. } => Icon::new(IconName::ZedPredict), })) .child( h_flex() @@ -9217,7 +9208,7 @@ impl Editor { )? } - None if is_refreshing => match &self.stale_inline_completion_in_menu { + None if is_refreshing => match &self.stale_edit_prediction_in_menu { Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview( stale_completion, cursor_point, @@ -9247,7 +9238,7 @@ impl Editor { completion.into_any_element() }; - let has_completion = self.active_inline_completion.is_some(); + let has_completion = self.active_edit_prediction.is_some(); let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; Some( @@ -9306,7 +9297,7 @@ impl Editor { fn render_edit_prediction_cursor_popover_preview( &self, - completion: &InlineCompletionState, + completion: &EditPredictionState, cursor_point: Point, style: &EditorStyle, cx: &mut Context, @@ -9334,7 +9325,7 @@ impl Editor { } match &completion.completion { - InlineCompletion::Move { + EditPrediction::Move { target, snapshot, .. } => Some( h_flex() @@ -9351,7 +9342,7 @@ impl Editor { .child(Label::new("Jump to Edit")), ), - InlineCompletion::Edit { + EditPrediction::Edit { edits, edit_preview, snapshot, @@ -9359,7 +9350,7 @@ impl Editor { } => { let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; - let (highlighted_edits, has_more_lines) = crate::inline_completion_edit_text( + let (highlighted_edits, has_more_lines) = crate::edit_prediction_edit_text( &snapshot, &edits, edit_preview.as_ref()?, @@ -9437,8 +9428,8 @@ impl Editor { cx.notify(); self.completion_tasks.clear(); let context_menu = self.context_menu.borrow_mut().take(); - self.stale_inline_completion_in_menu.take(); - self.update_visible_inline_completion(window, cx); + self.stale_edit_prediction_in_menu.take(); + self.update_visible_edit_prediction(window, cx); if let Some(CodeContextMenu::Completions(_)) = &context_menu { if let Some(completion_provider) = &self.completion_provider { completion_provider.selection_changed(None, window, cx); @@ -9796,7 +9787,7 @@ impl Editor { this.edit(edits, None, cx); }) } - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); linked_editing_ranges::refresh_linked_ranges(this, window, cx); }); } @@ -9815,7 +9806,7 @@ impl Editor { }) }); this.insert("", window, cx); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -9948,7 +9939,7 @@ impl Editor { self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); this.change_selections(Default::default(), window, cx, |s| s.select(selections)); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -12277,7 +12268,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); cx.emit(EditorEvent::TransactionUndone { transaction_id }); } @@ -12307,7 +12298,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); } } @@ -15294,7 +15285,7 @@ impl Editor { ]) }); self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); - self.refresh_inline_completion(false, true, window, cx); + self.refresh_edit_prediction(false, true, window, cx); } pub fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { @@ -16258,7 +16249,7 @@ impl Editor { font_weight: Some(FontWeight::BOLD), ..make_inlay_hints_style(cx.app) }, - inline_completion_styles: make_suggestion_styles( + edit_prediction_styles: make_suggestion_styles( cx.app, ), ..EditorStyle::default() @@ -19032,7 +19023,7 @@ impl Editor { (selection.range(), uuid.to_string()) }); this.edit(edits, cx); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -19885,8 +19876,8 @@ impl Editor { self.refresh_selected_text_highlights(true, window, cx); self.refresh_single_line_folds(window, cx); refresh_matching_bracket_highlights(self, window, cx); - if self.has_active_inline_completion() { - self.update_visible_inline_completion(window, cx); + if self.has_active_edit_prediction() { + self.update_visible_edit_prediction(window, cx); } if let Some(project) = self.project.as_ref() { if let Some(edited_buffer) = edited_buffer { @@ -20088,7 +20079,7 @@ impl Editor { } self.tasks_update_task = Some(self.refresh_runnables(window, cx)); self.update_edit_prediction_settings(cx); - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); self.refresh_inline_values(cx); self.refresh_inlay_hints( InlayHintRefreshReason::SettingsChange(inlay_hint_settings( @@ -20720,7 +20711,7 @@ impl Editor { { self.hide_context_menu(window, cx); } - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); cx.emit(EditorEvent::Blurred); cx.notify(); } @@ -22782,7 +22773,7 @@ impl Render for Editor { syntax: cx.theme().syntax().clone(), status: cx.theme().status().clone(), inlay_hints_style: make_inlay_hints_style(cx), - inline_completion_styles: make_suggestion_styles(cx), + edit_prediction_styles: make_suggestion_styles(cx), unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade, show_underlines: self.diagnostics_enabled(), }, @@ -23177,7 +23168,7 @@ impl InvalidationRegion for SnippetState { } } -fn inline_completion_edit_text( +fn edit_prediction_edit_text( current_snapshot: &BufferSnapshot, edits: &[(Range, String)], edit_preview: &EditPreview, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1a4f444275..1cb3565733 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::{ JoinLines, code_context_menus::CodeContextMenu, - inline_completion_tests::FakeInlineCompletionProvider, + edit_prediction_tests::FakeEditPredictionProvider, linked_editing_ranges::LinkedEditingRanges, scroll::scroll_amount::ScrollAmount, test::{ @@ -7251,12 +7251,12 @@ async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) { +async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider(Some(provider.clone()), window, cx); }); @@ -7279,7 +7279,7 @@ async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext cx.update(|_, cx| { provider.update(cx, |provider, _| { - provider.set_inline_completion(Some(inline_completion::InlineCompletion { + provider.set_edit_prediction(Some(edit_prediction::EditPrediction { id: None, edits: vec![(edit_position..edit_position, "X".into())], edit_preview: None, @@ -7287,7 +7287,7 @@ async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext }) }); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); cx.update_editor(|editor, window, cx| { editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx) }); @@ -20552,7 +20552,7 @@ async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContex } #[gpui::test] -async fn test_inline_completion_text(cx: &mut TestAppContext) { +async fn test_edit_prediction_text(cx: &mut TestAppContext) { init_test(cx, |_| {}); // Simple insertion @@ -20651,7 +20651,7 @@ async fn test_inline_completion_text(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) { +async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) { init_test(cx, |_| {}); // Deletion @@ -20741,7 +20741,7 @@ async fn assert_highlighted_edits( .await; cx.update(|_window, cx| { - let highlighted_edits = inline_completion_edit_text( + let highlighted_edits = edit_prediction_edit_text( &snapshot.as_singleton().unwrap().2, &edits, &edit_preview, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7e77f113ac..268855ab61 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3,11 +3,11 @@ use crate::{ CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, - EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, - FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, - HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, - LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, - PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, + EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot, + EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, + HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp, + MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, + PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, @@ -554,7 +554,7 @@ impl EditorElement { register_action(editor, window, Editor::signature_help_next); register_action(editor, window, Editor::next_edit_prediction); register_action(editor, window, Editor::previous_edit_prediction); - register_action(editor, window, Editor::show_inline_completion); + register_action(editor, window, Editor::show_edit_prediction); register_action(editor, window, Editor::context_menu_first); register_action(editor, window, Editor::context_menu_prev); register_action(editor, window, Editor::context_menu_next); @@ -562,7 +562,7 @@ impl EditorElement { register_action(editor, window, Editor::display_cursor_names); register_action(editor, window, Editor::unique_lines_case_insensitive); register_action(editor, window, Editor::unique_lines_case_sensitive); - register_action(editor, window, Editor::accept_partial_inline_completion); + register_action(editor, window, Editor::accept_partial_edit_prediction); register_action(editor, window, Editor::accept_edit_prediction); register_action(editor, window, Editor::restore_file); register_action(editor, window, Editor::git_restore); @@ -2093,7 +2093,7 @@ impl EditorElement { row_block_types: &HashMap, content_origin: gpui::Point, scroll_pixel_position: gpui::Point, - inline_completion_popover_origin: Option>, + edit_prediction_popover_origin: Option>, start_row: DisplayRow, end_row: DisplayRow, line_height: Pixels, @@ -2210,12 +2210,13 @@ impl EditorElement { cmp::max(padded_line, min_start) }; - let behind_inline_completion_popover = inline_completion_popover_origin - .as_ref() - .map_or(false, |inline_completion_popover_origin| { - (pos_y..pos_y + line_height).contains(&inline_completion_popover_origin.y) - }); - let opacity = if behind_inline_completion_popover { + let behind_edit_prediction_popover = edit_prediction_popover_origin.as_ref().map_or( + false, + |edit_prediction_popover_origin| { + (pos_y..pos_y + line_height).contains(&edit_prediction_popover_origin.y) + }, + ); + let opacity = if behind_edit_prediction_popover { 0.5 } else { 1.0 @@ -2427,9 +2428,9 @@ impl EditorElement { let mut padding = INLINE_BLAME_PADDING_EM_WIDTHS; - if let Some(inline_completion) = editor.active_inline_completion.as_ref() { - match &inline_completion.completion { - InlineCompletion::Edit { + if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() { + match &edit_prediction.completion { + EditPrediction::Edit { display_mode: EditDisplayMode::TabAccept, .. } => padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS, @@ -4086,8 +4087,7 @@ impl EditorElement { { let editor = self.editor.read(cx); - if editor - .edit_prediction_visible_in_cursor_popover(editor.has_active_inline_completion()) + if editor.edit_prediction_visible_in_cursor_popover(editor.has_active_edit_prediction()) { height_above_menu += editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING; @@ -6676,14 +6676,14 @@ impl EditorElement { } } - fn paint_inline_completion_popover( + fn paint_edit_prediction_popover( &mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App, ) { - if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() { - inline_completion_popover.paint(window, cx); + if let Some(edit_prediction_popover) = layout.edit_prediction_popover.as_mut() { + edit_prediction_popover.paint(window, cx); } } @@ -8501,7 +8501,7 @@ impl Element for EditorElement { ) }); - let (inline_completion_popover, inline_completion_popover_origin) = self + let (edit_prediction_popover, edit_prediction_popover_origin) = self .editor .update(cx, |editor, cx| { editor.render_edit_prediction_popover( @@ -8530,7 +8530,7 @@ impl Element for EditorElement { &row_block_types, content_origin, scroll_pixel_position, - inline_completion_popover_origin, + edit_prediction_popover_origin, start_row, end_row, line_height, @@ -8919,7 +8919,7 @@ impl Element for EditorElement { cursors, visible_cursors, selections, - inline_completion_popover, + edit_prediction_popover, diff_hunk_controls, mouse_context_menu, test_indicators, @@ -9001,7 +9001,7 @@ impl Element for EditorElement { self.paint_minimap(layout, window, cx); self.paint_scrollbars(layout, window, cx); - self.paint_inline_completion_popover(layout, window, cx); + self.paint_edit_prediction_popover(layout, window, cx); self.paint_mouse_context_menu(layout, window, cx); }); }) @@ -9102,7 +9102,7 @@ pub struct EditorLayout { expand_toggles: Vec)>>, diff_hunk_controls: Vec, crease_trailers: Vec>, - inline_completion_popover: Option, + edit_prediction_popover: Option, mouse_context_menu: Option, tab_invisible: ShapedLine, space_invisible: ShapedLine, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index b9b7cb2e58..a8850984a1 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -907,12 +907,12 @@ mod tests { let inlays = (0..buffer_snapshot.len()) .flat_map(|offset| { [ - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(&mut id), buffer_snapshot.anchor_at(offset, Bias::Left), "test", ), - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(&mut id), buffer_snapshot.anchor_at(offset, Bias::Right), "test", diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 2b0e13f4be..606f3a3f0e 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -253,8 +253,8 @@ impl LogStore { let copilot_subscription = Copilot::global(cx).map(|copilot| { let copilot = &copilot; - cx.subscribe(copilot, |this, copilot, inline_completion_event, cx| { - if let copilot::Event::CopilotLanguageServerStarted = inline_completion_event { + cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| { + if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event { if let Some(server) = copilot.read(cx).language_server() { let server_id = server.server_id(); let weak_this = cx.weak_entity(); diff --git a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs index c32da88229..646af8f63d 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs @@ -242,22 +242,22 @@ static STRING_REPLACE: LazyLock> = LazyLock::new(|| { "inline_completion::ToggleMenu", "edit_prediction::ToggleMenu", ), - ("editor::NextInlineCompletion", "editor::NextEditPrediction"), + ("editor::NextEditPrediction", "editor::NextEditPrediction"), ( - "editor::PreviousInlineCompletion", + "editor::PreviousEditPrediction", "editor::PreviousEditPrediction", ), ( - "editor::AcceptPartialInlineCompletion", + "editor::AcceptPartialEditPrediction", "editor::AcceptPartialEditPrediction", ), - ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"), + ("editor::ShowEditPrediction", "editor::ShowEditPrediction"), ( - "editor::AcceptInlineCompletion", + "editor::AcceptEditPrediction", "editor::AcceptEditPrediction", ), ( - "editor::ToggleInlineCompletions", + "editor::ToggleEditPredictions", "editor::ToggleEditPrediction", ), ]) diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 2f77b4f3cc..ebec96dd7b 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -1101,7 +1101,7 @@ impl RulesLibrary { inlay_hints_style: editor::make_inlay_hints_style( cx, ), - inline_completion_styles: + edit_prediction_styles: editor::make_suggestion_styles(cx), ..EditorStyle::default() }, diff --git a/crates/supermaven/Cargo.toml b/crates/supermaven/Cargo.toml index d0451f34f2..4fc6a618ff 100644 --- a/crates/supermaven/Cargo.toml +++ b/crates/supermaven/Cargo.toml @@ -16,9 +16,9 @@ doctest = false anyhow.workspace = true client.workspace = true collections.workspace = true +edit_prediction.workspace = true futures.workspace = true gpui.workspace = true -inline_completion.workspace = true language.workspace = true log.workspace = true postage.workspace = true diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index c49272e66e..2660a03e6f 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -1,8 +1,8 @@ use crate::{Supermaven, SupermavenCompletionStateId}; use anyhow::Result; +use edit_prediction::{Direction, EditPrediction, EditPredictionProvider}; use futures::StreamExt as _; use gpui::{App, Context, Entity, EntityId, Task}; -use inline_completion::{Direction, EditPredictionProvider, InlineCompletion}; use language::{Anchor, Buffer, BufferSnapshot}; use project::Project; use std::{ @@ -44,7 +44,7 @@ fn completion_from_diff( completion_text: &str, position: Anchor, delete_range: Range, -) -> InlineCompletion { +) -> EditPrediction { let buffer_text = snapshot .text_for_range(delete_range.clone()) .collect::(); @@ -91,7 +91,7 @@ fn completion_from_diff( edits.push((edit_range, edit_text)); } - InlineCompletion { + EditPrediction { id: None, edits, edit_preview: None, @@ -182,7 +182,7 @@ impl EditPredictionProvider for SupermavenCompletionProvider { buffer: &Entity, cursor_position: Anchor, cx: &mut Context, - ) -> Option { + ) -> Option { let completion_text = self .supermaven .read(cx) diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs index dfe167fcd4..735a1310ae 100644 --- a/crates/telemetry_events/src/telemetry_events.rs +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -94,8 +94,8 @@ impl Display for AssistantPhase { pub enum Event { Flexible(FlexibleEvent), Editor(EditorEvent), - InlineCompletion(InlineCompletionEvent), - InlineCompletionRating(InlineCompletionRatingEvent), + EditPrediction(EditPredictionEvent), + EditPredictionRating(EditPredictionRatingEvent), Call(CallEvent), Assistant(AssistantEventData), Cpu(CpuEvent), @@ -132,7 +132,7 @@ pub struct EditorEvent { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct InlineCompletionEvent { +pub struct EditPredictionEvent { /// Provider of the completion suggestion (e.g. copilot, supermaven) pub provider: String, pub suggestion_accepted: bool, @@ -140,14 +140,14 @@ pub struct InlineCompletionEvent { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum InlineCompletionRating { +pub enum EditPredictionRating { Positive, Negative, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct InlineCompletionRatingEvent { - pub rating: InlineCompletionRating, +pub struct EditPredictionRatingEvent { + pub rating: EditPredictionRating, pub input_events: Arc, pub input_excerpt: Arc, pub output_excerpt: Arc, diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index c22cf0ef00..0e487f4410 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -3808,7 +3808,7 @@ mod test { cx.update_editor(|editor, _window, cx| { let range = editor.selections.newest_anchor().range(); let inlay_text = " field: int,\n field2: string\n field3: float"; - let inlay = Inlay::inline_completion(1, range.start, inlay_text); + let inlay = Inlay::edit_prediction(1, range.start, inlay_text); editor.splice_inlays(&[], vec![inlay], cx); }); @@ -3840,7 +3840,7 @@ mod test { let end_of_line = snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0)))); let inlay_text = " hint"; - let inlay = Inlay::inline_completion(1, end_of_line, inlay_text); + let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text); editor.splice_inlays(&[], vec![inlay], cx); }); cx.simulate_keystrokes("$"); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 135cdd687f..c1bc7a70ae 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -90,7 +90,7 @@ impl Vim { if let Some(kind) = motion_kind { vim.copy_selections_content(editor, kind, window, cx); editor.insert("", window, cx); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); } }); }); @@ -123,7 +123,7 @@ impl Vim { if objects_found { vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx); editor.insert("", window, cx); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); } }); }); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index ccbb3dd0fd..2cf40292cf 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -82,7 +82,7 @@ impl Vim { selection.collapse_to(cursor, selection.goal) }); }); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); }); } @@ -169,7 +169,7 @@ impl Vim { selection.collapse_to(cursor, selection.goal) }); }); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); }); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 2f759ec8af..72edbe77ed 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1741,11 +1741,11 @@ impl Vim { editor.set_autoindent(vim.should_autoindent()); editor.selections.line_mode = matches!(vim.mode, Mode::VisualLine); - let hide_inline_completions = match vim.mode { + let hide_edit_predictions = match vim.mode { Mode::Insert | Mode::Replace => false, _ => true, }; - editor.set_inline_completions_hidden_for_vim_mode(hide_inline_completions, window, cx); + editor.set_edit_predictions_hidden_for_vim_mode(hide_edit_predictions, window, cx); }); cx.notify() } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 536af7b7b9..bdd8db9027 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -76,7 +76,7 @@ gpui_tokio.workspace = true http_client.workspace = true image_viewer.workspace = true indoc.workspace = true -inline_completion_button.workspace = true +edit_prediction_button.workspace = true inspector_ui.workspace = true install_cli.workspace = true jj_ui.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c264135e5c..825aea615f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -51,9 +51,9 @@ use workspace::{ }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, - derive_paths_with_position, handle_cli_connection, handle_keymap_file_changes, - handle_settings_changed, handle_settings_file_changes, initialize_workspace, - inline_completion_registry, open_paths_with_positions, + derive_paths_with_position, edit_prediction_registry, handle_cli_connection, + handle_keymap_file_changes, handle_settings_changed, handle_settings_file_changes, + initialize_workspace, open_paths_with_positions, }; use crate::zed::OpenRequestKind; @@ -559,11 +559,7 @@ pub fn main() { web_search::init(cx); web_search_providers::init(app_state.client.clone(), cx); snippet_provider::init(cx); - inline_completion_registry::init( - app_state.client.clone(), - app_state.user_store.clone(), - cx, - ); + edit_prediction_registry::init(app_state.client.clone(), app_state.user_store.clone(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx); agent_ui::init( app_state.fs.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index af317edeee..ec62ed33fd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,6 +1,6 @@ mod app_menus; pub mod component_preview; -pub mod inline_completion_registry; +pub mod edit_prediction_registry; #[cfg(target_os = "macos")] pub(crate) mod mac_only_instance; mod migrate; @@ -332,18 +332,18 @@ pub fn initialize_workspace( show_software_emulation_warning_if_needed(specs, window, cx); } - let inline_completion_menu_handle = PopoverMenuHandle::default(); + let edit_prediction_menu_handle = PopoverMenuHandle::default(); let edit_prediction_button = cx.new(|cx| { - inline_completion_button::InlineCompletionButton::new( + edit_prediction_button::EditPredictionButton::new( app_state.fs.clone(), app_state.user_store.clone(), - inline_completion_menu_handle.clone(), + edit_prediction_menu_handle.clone(), cx, ) }); workspace.register_action({ - move |_, _: &inline_completion_button::ToggleMenu, window, cx| { - inline_completion_menu_handle.toggle(window, cx); + move |_, _: &edit_prediction_button::ToggleMenu, window, cx| { + edit_prediction_menu_handle.toggle(window, cx); } }); diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs similarity index 96% rename from crates/zed/src/zed/inline_completion_registry.rs rename to crates/zed/src/zed/edit_prediction_registry.rs index bbecd26417..b9f561c0e7 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -11,7 +11,7 @@ use supermaven::{Supermaven, SupermavenCompletionProvider}; use ui::Window; use util::ResultExt; use workspace::Workspace; -use zeta::{ProviderDataCollection, ZetaInlineCompletionProvider}; +use zeta::{ProviderDataCollection, ZetaEditPredictionProvider}; pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let editors: Rc, AnyWindowHandle>>> = Rc::default(); @@ -171,7 +171,7 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context| { - editor.show_inline_completion(&Default::default(), window, cx); + editor.show_edit_prediction(&Default::default(), window, cx); }, )) .detach(); @@ -207,7 +207,7 @@ fn assign_edit_prediction_provider( match provider { EditPredictionProvider::None => { - editor.set_edit_prediction_provider::(None, window, cx); + editor.set_edit_prediction_provider::(None, window, cx); } EditPredictionProvider::Copilot => { if let Some(copilot) = Copilot::global(cx) { @@ -265,7 +265,7 @@ fn assign_edit_prediction_provider( ProviderDataCollection::new(zeta.clone(), singleton_buffer, cx); let provider = - cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta, data_collection)); + cx.new(|_| zeta::ZetaEditPredictionProvider::new(zeta, data_collection)); editor.set_edit_prediction_provider(Some(provider), window, cx); } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 7ab7293573..e76bef59a3 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -381,7 +381,7 @@ impl Render for QuickActionBar { } if has_edit_prediction_provider { - let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions") + let mut edit_prediction_entry = ContextMenuEntry::new("Edit Predictions") .toggleable(IconPosition::Start, edit_predictions_enabled_at_cursor && show_edit_predictions) .disabled(!edit_predictions_enabled_at_cursor) .action( @@ -401,12 +401,12 @@ impl Render for QuickActionBar { } }); if !edit_predictions_enabled_at_cursor { - inline_completion_entry = inline_completion_entry.documentation_aside(DocumentationSide::Left, |_| { + edit_prediction_entry = edit_prediction_entry.documentation_aside(DocumentationSide::Left, |_| { Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element() }); } - menu = menu.item(inline_completion_entry); + menu = menu.item(edit_prediction_entry); } menu = menu.separator(); diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index 26eeda3f22..9f1d02b790 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -33,7 +33,7 @@ futures.workspace = true gpui.workspace = true http_client.workspace = true indoc.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true language.workspace = true language_model.workspace = true log.workspace = true diff --git a/crates/zeta/src/completion_diff_element.rs b/crates/zeta/src/completion_diff_element.rs index 3b7355d797..73c3cb20cd 100644 --- a/crates/zeta/src/completion_diff_element.rs +++ b/crates/zeta/src/completion_diff_element.rs @@ -1,6 +1,6 @@ use std::cmp; -use crate::InlineCompletion; +use crate::EditPrediction; use gpui::{ AnyElement, App, BorderStyle, Bounds, Corners, Edges, HighlightStyle, Hsla, StyledText, TextLayout, TextStyle, point, prelude::*, quad, size, @@ -17,7 +17,7 @@ pub struct CompletionDiffElement { } impl CompletionDiffElement { - pub fn new(completion: &InlineCompletion, cx: &App) -> Self { + pub fn new(completion: &EditPrediction, cx: &App) -> Self { let mut diff = completion .snapshot .text_for_range(completion.excerpt_range.clone()) diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index 5a873fb8de..ac7fcade91 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -1,4 +1,4 @@ -use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta}; +use crate::{CompletionDiffElement, EditPrediction, EditPredictionRating, Zeta}; use editor::Editor; use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, actions, prelude::*}; use language::language_settings; @@ -34,7 +34,7 @@ pub struct RateCompletionModal { } struct ActiveCompletion { - completion: InlineCompletion, + completion: EditPrediction, feedback_editor: Entity, } @@ -157,7 +157,7 @@ impl RateCompletionModal { if let Some(active) = &self.active_completion { zeta.rate_completion( &active.completion, - InlineCompletionRating::Positive, + EditPredictionRating::Positive, active.feedback_editor.read(cx).text(cx), cx, ); @@ -189,7 +189,7 @@ impl RateCompletionModal { self.zeta.update(cx, |zeta, cx| { zeta.rate_completion( &active.completion, - InlineCompletionRating::Negative, + EditPredictionRating::Negative, active.feedback_editor.read(cx).text(cx), cx, ); @@ -250,7 +250,7 @@ impl RateCompletionModal { pub fn select_completion( &mut self, - completion: Option, + completion: Option, focus: bool, window: &mut Window, cx: &mut Context, diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index f130c3a965..1cd8e8d17f 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -8,8 +8,8 @@ mod rate_completion_modal; pub(crate) use completion_diff_element::*; use db::kvp::{Dismissable, KEY_VALUE_STORE}; +use edit_prediction::DataCollectionState; pub use init::*; -use inline_completion::DataCollectionState; use license_detection::LICENSE_FILES_TO_CHECK; pub use license_detection::is_license_eligible_for_data_collection; pub use rate_completion_modal::*; @@ -50,7 +50,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use telemetry_events::InlineCompletionRating; +use telemetry_events::EditPredictionRating; use thiserror::Error; use util::ResultExt; use uuid::Uuid; @@ -81,15 +81,15 @@ actions!( ); #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)] -pub struct InlineCompletionId(Uuid); +pub struct EditPredictionId(Uuid); -impl From for gpui::ElementId { - fn from(value: InlineCompletionId) -> Self { +impl From for gpui::ElementId { + fn from(value: EditPredictionId) -> Self { gpui::ElementId::Uuid(value.0) } } -impl std::fmt::Display for InlineCompletionId { +impl std::fmt::Display for EditPredictionId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } @@ -134,8 +134,8 @@ struct ZetaGlobal(Entity); impl Global for ZetaGlobal {} #[derive(Clone)] -pub struct InlineCompletion { - id: InlineCompletionId, +pub struct EditPrediction { + id: EditPredictionId, path: Arc, excerpt_range: Range, cursor_offset: usize, @@ -150,7 +150,7 @@ pub struct InlineCompletion { response_received_at: Instant, } -impl InlineCompletion { +impl EditPrediction { fn latency(&self) -> Duration { self.response_received_at .duration_since(self.buffer_snapshotted_at) @@ -207,9 +207,9 @@ fn interpolate( if edits.is_empty() { None } else { Some(edits) } } -impl std::fmt::Debug for InlineCompletion { +impl std::fmt::Debug for EditPrediction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("InlineCompletion") + f.debug_struct("EditPrediction") .field("id", &self.id) .field("path", &self.path) .field("edits", &self.edits) @@ -222,8 +222,8 @@ pub struct Zeta { client: Arc, events: VecDeque, registered_buffers: HashMap, - shown_completions: VecDeque, - rated_completions: HashSet, + shown_completions: VecDeque, + rated_completions: HashSet, data_collection_choice: Entity, llm_token: LlmApiToken, _llm_token_subscription: Subscription, @@ -384,7 +384,7 @@ impl Zeta { can_collect_data: bool, cx: &mut Context, perform_predict_edits: F, - ) -> Task>> + ) -> Task>> where F: FnOnce(PerformPredictEditsParams) -> R + 'static, R: Future)>> @@ -664,7 +664,7 @@ and then another position: language::Anchor, response: PredictEditsResponse, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { use std::future::ready; self.request_completion_impl(None, project, buffer, position, false, cx, |_params| { @@ -679,7 +679,7 @@ and then another position: language::Anchor, can_collect_data: bool, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { let workspace = self .workspace .as_ref() @@ -773,7 +773,7 @@ and then another fn accept_edit_prediction( &mut self, - request_id: InlineCompletionId, + request_id: EditPredictionId, cx: &mut Context, ) -> Task> { let client = self.client.clone(); @@ -852,7 +852,7 @@ and then another input_excerpt: String, buffer_snapshotted_at: Instant, cx: &AsyncApp, - ) -> Task>> { + ) -> Task>> { let snapshot = snapshot.clone(); let request_id = prediction_response.request_id; let output_excerpt = prediction_response.output_excerpt; @@ -884,8 +884,8 @@ and then another let edit_preview = edit_preview.await; - Ok(Some(InlineCompletion { - id: InlineCompletionId(request_id), + Ok(Some(EditPrediction { + id: EditPredictionId(request_id), path, excerpt_range: editable_range, cursor_offset, @@ -995,11 +995,11 @@ and then another .collect() } - pub fn is_completion_rated(&self, completion_id: InlineCompletionId) -> bool { + pub fn is_completion_rated(&self, completion_id: EditPredictionId) -> bool { self.rated_completions.contains(&completion_id) } - pub fn completion_shown(&mut self, completion: &InlineCompletion, cx: &mut Context) { + pub fn completion_shown(&mut self, completion: &EditPrediction, cx: &mut Context) { self.shown_completions.push_front(completion.clone()); if self.shown_completions.len() > 50 { let completion = self.shown_completions.pop_back().unwrap(); @@ -1010,8 +1010,8 @@ and then another pub fn rate_completion( &mut self, - completion: &InlineCompletion, - rating: InlineCompletionRating, + completion: &EditPrediction, + rating: EditPredictionRating, feedback: String, cx: &mut Context, ) { @@ -1029,7 +1029,7 @@ and then another cx.notify(); } - pub fn shown_completions(&self) -> impl DoubleEndedIterator { + pub fn shown_completions(&self) -> impl DoubleEndedIterator { self.shown_completions.iter() } @@ -1323,12 +1323,12 @@ impl Event { } #[derive(Debug, Clone)] -struct CurrentInlineCompletion { +struct CurrentEditPrediction { buffer_id: EntityId, - completion: InlineCompletion, + completion: EditPrediction, } -impl CurrentInlineCompletion { +impl CurrentEditPrediction { fn should_replace_completion(&self, old_completion: &Self, snapshot: &BufferSnapshot) -> bool { if self.buffer_id != old_completion.buffer_id { return true; @@ -1497,17 +1497,17 @@ async fn llm_token_retry( } } -pub struct ZetaInlineCompletionProvider { +pub struct ZetaEditPredictionProvider { zeta: Entity, pending_completions: ArrayVec, next_pending_completion_id: usize, - current_completion: Option, + current_completion: Option, /// None if this is entirely disabled for this provider provider_data_collection: ProviderDataCollection, last_request_timestamp: Instant, } -impl ZetaInlineCompletionProvider { +impl ZetaEditPredictionProvider { pub const THROTTLE_TIMEOUT: Duration = Duration::from_millis(300); pub fn new(zeta: Entity, provider_data_collection: ProviderDataCollection) -> Self { @@ -1522,7 +1522,7 @@ impl ZetaInlineCompletionProvider { } } -impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider { +impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { fn name() -> &'static str { "zed-predict" } @@ -1650,7 +1650,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider Ok(completion_request) => { let completion_request = completion_request.await; completion_request.map(|c| { - c.map(|completion| CurrentInlineCompletion { + c.map(|completion| CurrentEditPrediction { buffer_id: buffer.entity_id(), completion, }) @@ -1723,7 +1723,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider &mut self, _buffer: Entity, _cursor_position: language::Anchor, - _direction: inline_completion::Direction, + _direction: edit_prediction::Direction, _cx: &mut Context, ) { // Right now we don't support cycling. @@ -1754,8 +1754,8 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider buffer: &Entity, cursor_position: language::Anchor, cx: &mut Context, - ) -> Option { - let CurrentInlineCompletion { + ) -> Option { + let CurrentEditPrediction { buffer_id, completion, .. @@ -1803,7 +1803,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider } } - Some(inline_completion::InlineCompletion { + Some(edit_prediction::EditPrediction { id: Some(completion.id.to_string().into()), edits: edits[edit_start_ix..edit_end_ix].to_vec(), edit_preview: Some(completion.edit_preview.clone()), @@ -1833,7 +1833,7 @@ mod tests { use super::*; #[gpui::test] - async fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) { + async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) { let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx)); let edits: Arc<[(Range, String)]> = cx.update(|cx| { to_completion_edits( @@ -1848,12 +1848,12 @@ mod tests { .read(|cx| buffer.read(cx).preview_edits(edits.clone(), cx)) .await; - let completion = InlineCompletion { + let completion = EditPrediction { edits, edit_preview, path: Path::new("").into(), snapshot: cx.read(|cx| buffer.read(cx).snapshot()), - id: InlineCompletionId(Uuid::new_v4()), + id: EditPredictionId(Uuid::new_v4()), excerpt_range: 0..0, cursor_offset: 0, input_outline: "".into(), @@ -2014,7 +2014,7 @@ mod tests { } #[gpui::test] - async fn test_inline_completion_end_of_buffer(cx: &mut TestAppContext) { + async fn test_edit_prediction_end_of_buffer(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index a6e6f7c774..04646213e6 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -220,7 +220,7 @@ Alternatively, you can provide an OAuth token via the `GH_COPILOT_TOKEN` environ > **Note**: If you don't see specific models in the dropdown, you may need to enable them in your [GitHub Copilot settings](https://github.com/settings/copilot/features). -To use Copilot Enterprise with Zed (for both agent and inline completions), you must configure your enterprise endpoint as described in [Configuring GitHub Copilot Enterprise](./edit-prediction.md#github-copilot-enterprise). +To use Copilot Enterprise with Zed (for both agent and completions), you must configure your enterprise endpoint as described in [Configuring GitHub Copilot Enterprise](./edit-prediction.md#github-copilot-enterprise). ### Google AI {#google-ai} diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 9984f234ad..feed912787 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -93,7 +93,7 @@ For example: # in an editor, it might look like this: Workspace os=macos keyboard_layout=com.apple.keylayout.QWERTY Pane - Editor mode=full extension=md inline_completion vim_mode=insert + Editor mode=full extension=md vim_mode=insert # in the project panel Workspace os=macos From bf361c316d43469a568d5a621eccf5de9b42b906 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:25:42 +0200 Subject: [PATCH 12/46] search: Update results multi-buffer before search is finished (#35470) I'm not sure when we've lost that notify, but it's causing the time to first search result equal to the time to run the whole search, which is not great. Co-authored-by: Remco This discussion has originally started in #35444 Release Notes: - Improved project search speed. Co-authored-by: Remco --- crates/search/src/project_search.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 3b9700c5f1..15c1099aec 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -355,8 +355,9 @@ impl ProjectSearch { while let Some(new_ranges) = new_ranges.next().await { project_search - .update(cx, |project_search, _| { + .update(cx, |project_search, cx| { project_search.match_ranges.extend(new_ranges); + cx.notify(); }) .ok()?; } From 2c8f144e6b26d029e64821dfcabc3a6c060329a1 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 4 Aug 2025 23:08:22 +0530 Subject: [PATCH 13/46] workspace: Fix not able to close tab when buffer save fails (#35589) Closes #26216, Closes #35517 Now we prompt user if buffer save failed, asking them to close without saving or cancel the action. Release Notes: - Fixed issue where closing read-only or deleted buffer would not close that tab. Co-authored-by: Lukas Wirth --- crates/workspace/src/pane.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ad1c74a040..2062255f4b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1664,10 +1664,33 @@ impl Pane { } if should_save { - if !Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx) - .await? + match Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx) + .await { - break; + Ok(success) => { + if !success { + break; + } + } + Err(err) => { + let answer = pane.update_in(cx, |_, window, cx| { + let detail = Self::file_names_for_prompt( + &mut [&item_to_close].into_iter(), + cx, + ); + window.prompt( + PromptLevel::Warning, + &format!("Unable to save file: {}", &err), + Some(&detail), + &["Close Without Saving", "Cancel"], + cx, + ) + })?; + match answer.await { + Ok(0) => {} + Ok(1..) | Err(_) => break, + } + } } } From fa8dd1c54740dbab38e2b4488d51c89d7ad6bb03 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:01:32 -0300 Subject: [PATCH 14/46] agent: Adjust full screen menu item label and background color (#35592) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 1 + crates/agent_ui/src/agent_panel.rs | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 24d8b73396..9ea9209189 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2453,6 +2453,7 @@ impl Render for AcpThreadView { .on_action(cx.listener(Self::previous_history_message)) .on_action(cx.listener(Self::next_history_message)) .on_action(cx.listener(Self::open_agent_diff)) + .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { connection } => v_flex() .p_2() diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 4751eff15e..5f3315f69a 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1880,10 +1880,10 @@ impl AgentPanel { }), ); - let zoom_in_label = if self.is_zoomed(window, cx) { - "Zoom Out" + let full_screen_label = if self.is_zoomed(window, cx) { + "Disable Full Screen" } else { - "Zoom In" + "Enable Full Screen" }; let active_thread = match &self.active_view { @@ -2071,7 +2071,8 @@ impl AgentPanel { menu = menu .action("Rules…", Box::new(OpenRulesLibrary::default())) .action("Settings", Box::new(OpenSettings)) - .action(zoom_in_label, Box::new(ToggleZoom)); + .separator() + .action(full_screen_label, Box::new(ToggleZoom)); menu })) } From 9fa634f02faeec4ac0c428e55ba01755472b98f5 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:08:00 -0400 Subject: [PATCH 15/46] git: Add option to branch from default branch in branch picker (#34663) Closes #33700 The option shows up as an icon that appears on entries that would create a new branch. You can also branch from the default by secondary confirming, which the icon has a tooltip for as well. We based the default branch on the results from this command: `git symbolic-ref refs/remotes/upstream/HEAD` and fallback to `git symbolic-ref refs/remotes/origin/HEAD` Release Notes: - Add option to create a branch from a default branch in git branch picker --------- Co-authored-by: Cole Miller --- crates/fs/src/fake_git_repo.rs | 6 ++- crates/git/src/repository.rs | 33 ++++++++++++++++ crates/git_ui/src/branch_picker.rs | 60 ++++++++++++++++++++++++++++-- crates/project/src/git_store.rs | 19 ++++++++++ crates/proto/proto/git.proto | 9 +++++ crates/proto/proto/zed.proto | 5 ++- crates/proto/src/proto.rs | 10 +++-- 7 files changed, 133 insertions(+), 9 deletions(-) diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 378a8fb7df..04ba656232 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -10,7 +10,7 @@ use git::{ }, status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus}, }; -use gpui::{AsyncApp, BackgroundExecutor}; +use gpui::{AsyncApp, BackgroundExecutor, SharedString}; use ignore::gitignore::GitignoreBuilder; use rope::Rope; use smol::future::FutureExt as _; @@ -491,4 +491,8 @@ impl GitRepository for FakeGitRepository { ) -> BoxFuture<'_, Result> { unimplemented!() } + + fn default_branch(&self) -> BoxFuture<'_, Result>> { + unimplemented!() + } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index a63315e69e..b536bed710 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -463,6 +463,8 @@ pub trait GitRepository: Send + Sync { base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result>; + + fn default_branch(&self) -> BoxFuture<'_, Result>>; } pub enum DiffType { @@ -1607,6 +1609,37 @@ impl GitRepository for RealGitRepository { }) .boxed() } + + fn default_branch(&self) -> BoxFuture<'_, Result>> { + let working_directory = self.working_directory(); + let git_binary_path = self.git_binary_path.clone(); + + let executor = self.executor.clone(); + self.executor + .spawn(async move { + let working_directory = working_directory?; + let git = GitBinary::new(git_binary_path, working_directory, executor); + + if let Ok(output) = git + .run(&["symbolic-ref", "refs/remotes/upstream/HEAD"]) + .await + { + let output = output + .strip_prefix("refs/remotes/upstream/") + .map(|s| SharedString::from(s.to_owned())); + return Ok(output); + } + + let output = git + .run(&["symbolic-ref", "refs/remotes/origin/HEAD"]) + .await?; + + Ok(output + .strip_prefix("refs/remotes/origin/") + .map(|s| SharedString::from(s.to_owned()))) + }) + .boxed() + } } fn git_status_args(path_prefixes: &[RepoPath]) -> Vec { diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 9eac3ce5af..1092ba33d1 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -13,7 +13,7 @@ use project::git_store::Repository; use std::sync::Arc; use time::OffsetDateTime; use time_format::format_local_timestamp; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; +use ui::{HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; @@ -90,11 +90,21 @@ impl BranchList { let all_branches_request = repository .clone() .map(|repository| repository.update(cx, |repository, _| repository.branches())); + let default_branch_request = repository + .clone() + .map(|repository| repository.update(cx, |repository, _| repository.default_branch())); cx.spawn_in(window, async move |this, cx| { let mut all_branches = all_branches_request .context("No active repository")? .await??; + let default_branch = default_branch_request + .context("No active repository")? + .await + .map(Result::ok) + .ok() + .flatten() + .flatten(); let all_branches = cx .background_spawn(async move { @@ -124,6 +134,7 @@ impl BranchList { this.update_in(cx, |this, window, cx| { this.picker.update(cx, |picker, cx| { + picker.delegate.default_branch = default_branch; picker.delegate.all_branches = Some(all_branches); picker.refresh(window, cx); }) @@ -192,6 +203,7 @@ struct BranchEntry { pub struct BranchListDelegate { matches: Vec, all_branches: Option>, + default_branch: Option, repo: Option>, style: BranchListStyle, selected_index: usize, @@ -206,6 +218,7 @@ impl BranchListDelegate { repo, style, all_branches: None, + default_branch: None, selected_index: 0, last_query: Default::default(), modifiers: Default::default(), @@ -214,6 +227,7 @@ impl BranchListDelegate { fn create_branch( &self, + from_branch: Option, new_branch_name: SharedString, window: &mut Window, cx: &mut Context>, @@ -223,6 +237,11 @@ impl BranchListDelegate { }; let new_branch_name = new_branch_name.to_string().replace(' ', "-"); cx.spawn(async move |_, cx| { + if let Some(based_branch) = from_branch { + repo.update(cx, |repo, _| repo.change_branch(based_branch.to_string()))? + .await??; + } + repo.update(cx, |repo, _| { repo.create_branch(new_branch_name.to_string()) })? @@ -353,12 +372,22 @@ impl PickerDelegate for BranchListDelegate { }) } - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(entry) = self.matches.get(self.selected_index()) else { return; }; if entry.is_new { - self.create_branch(entry.branch.name().to_owned().into(), window, cx); + let from_branch = if secondary { + self.default_branch.clone() + } else { + None + }; + self.create_branch( + from_branch, + entry.branch.name().to_owned().into(), + window, + cx, + ); return; } @@ -439,6 +468,28 @@ impl PickerDelegate for BranchListDelegate { }) .unwrap_or_else(|| (None, None)); + let icon = if let Some(default_branch) = self.default_branch.clone() + && entry.is_new + { + Some( + IconButton::new("branch-from-default", IconName::GitBranchSmall) + .on_click(cx.listener(move |this, _, window, cx| { + this.delegate.set_selected_index(ix, window, cx); + this.delegate.confirm(true, window, cx); + })) + .tooltip(move |window, cx| { + Tooltip::for_action( + format!("Create branch based off default: {default_branch}"), + &menu::SecondaryConfirm, + window, + cx, + ) + }), + ) + } else { + None + }; + let branch_name = if entry.is_new { h_flex() .gap_1() @@ -504,7 +555,8 @@ impl PickerDelegate for BranchListDelegate { .color(Color::Muted) })) }), - ), + ) + .end_slot::(icon), ) } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 28dd0e91e3..c9f0fc7959 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -4025,6 +4025,25 @@ impl Repository { }) } + pub fn default_branch(&mut self) -> oneshot::Receiver>> { + let id = self.id; + self.send_job(None, move |repo, _| async move { + match repo { + RepositoryState::Local { backend, .. } => backend.default_branch().await, + RepositoryState::Remote { project_id, client } => { + let response = client + .request(proto::GetDefaultBranch { + project_id: project_id.0, + repository_id: id.to_proto(), + }) + .await?; + + anyhow::Ok(response.branch.map(SharedString::from)) + } + } + }) + } + pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver> { let id = self.id; self.send_job(None, move |repo, _cx| async move { diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index ea08d36371..c32da9b110 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -422,3 +422,12 @@ message BlameBufferResponse { reserved 1 to 4; } + +message GetDefaultBranch { + uint64 project_id = 1; + uint64 repository_id = 2; +} + +message GetDefaultBranchResponse { + optional string branch = 1; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 29ab2b1e90..d511ea5e8f 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -399,7 +399,10 @@ message Envelope { GetColorPresentationResponse get_color_presentation_response = 356; Stash stash = 357; - StashPop stash_pop = 358; // current max + StashPop stash_pop = 358; + + GetDefaultBranch get_default_branch = 359; + GetDefaultBranchResponse get_default_branch_response = 360; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 83e5a77c86..72b3807deb 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -315,7 +315,9 @@ messages!( (LogToDebugConsole, Background), (GetDocumentDiagnostics, Background), (GetDocumentDiagnosticsResponse, Background), - (PullWorkspaceDiagnostics, Background) + (PullWorkspaceDiagnostics, Background), + (GetDefaultBranch, Background), + (GetDefaultBranchResponse, Background), ); request_messages!( @@ -483,7 +485,8 @@ request_messages!( (GetDebugAdapterBinary, DebugAdapterBinary), (RunDebugLocators, DebugRequest), (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), - (PullWorkspaceDiagnostics, Ack) + (PullWorkspaceDiagnostics, Ack), + (GetDefaultBranch, GetDefaultBranchResponse), ); entity_messages!( @@ -615,7 +618,8 @@ entity_messages!( GetDebugAdapterBinary, LogToDebugConsole, GetDocumentDiagnostics, - PullWorkspaceDiagnostics + PullWorkspaceDiagnostics, + GetDefaultBranch ); entity_messages!( From 0ea4016e6613e9102baab34f7e128756aeb6776e Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:16:52 -0300 Subject: [PATCH 16/46] onboarding: Adjust skip button as flow progresses (#35596) Release Notes: - N/A --- crates/onboarding/src/onboarding.rs | 131 +++++++++++++++------------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index f7e76f2f34..a79d1d5aef 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -254,6 +254,40 @@ impl Onboarding { cx.emit(ItemEvent::UpdateTab); } + fn go_to_welcome_page(&self, cx: &mut App) { + with_active_or_new_workspace(cx, |workspace, window, cx| { + let Some((onboarding_id, onboarding_idx)) = workspace + .active_pane() + .read(cx) + .items() + .enumerate() + .find_map(|(idx, item)| { + let _ = item.downcast::()?; + Some((item.item_id(), idx)) + }) + else { + return; + }; + + workspace.active_pane().update(cx, |pane, cx| { + // Get the index here to get around the borrow checker + let idx = pane.items().enumerate().find_map(|(idx, item)| { + let _ = item.downcast::()?; + Some(idx) + }); + + if let Some(idx) = idx { + pane.activate_item(idx, true, true, window, cx); + } else { + let item = Box::new(WelcomePage::new(window, cx)); + pane.add_item(item, true, true, Some(onboarding_idx), window, cx); + } + + pane.remove_item(onboarding_id, false, false, window, cx); + }); + }); + } + fn render_nav_buttons( &mut self, window: &mut Window, @@ -319,6 +353,8 @@ impl Onboarding { } fn render_nav(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let ai_setup_page = matches!(self.selected_page, SelectedPage::AiSetup); + v_flex() .h_full() .w(rems_from_px(220.)) @@ -357,67 +393,38 @@ impl Onboarding { .gap_1() .children(self.render_nav_buttons(window, cx)), ) - .child( - ButtonLike::new("skip_all") - .child(Label::new("Skip All").ml_1()) - .on_click(|_, _, cx| { - with_active_or_new_workspace( - cx, - |workspace, window, cx| { - let Some((onboarding_id, onboarding_idx)) = - workspace - .active_pane() - .read(cx) - .items() - .enumerate() - .find_map(|(idx, item)| { - let _ = - item.downcast::()?; - Some((item.item_id(), idx)) - }) - else { - return; - }; - - workspace.active_pane().update(cx, |pane, cx| { - // Get the index here to get around the borrow checker - let idx = pane.items().enumerate().find_map( - |(idx, item)| { - let _ = - item.downcast::()?; - Some(idx) - }, - ); - - if let Some(idx) = idx { - pane.activate_item( - idx, true, true, window, cx, - ); - } else { - let item = - Box::new(WelcomePage::new(window, cx)); - pane.add_item( - item, - true, - true, - Some(onboarding_idx), - window, - cx, - ); - } - - pane.remove_item( - onboarding_id, - false, - false, - window, - cx, - ); - }); - }, - ); - }), - ), + .map(|this| { + if ai_setup_page { + this.child( + ButtonLike::new("start_building") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .child( + h_flex() + .ml_1() + .w_full() + .justify_between() + .child(Label::new("Start Building")) + .child( + Icon::new(IconName::Check) + .size(IconSize::Small), + ), + ) + .on_click(cx.listener(|this, _, _, cx| { + this.go_to_welcome_page(cx); + })), + ) + } else { + this.child( + ButtonLike::new("skip_all") + .size(ButtonSize::Medium) + .child(Label::new("Skip All").ml_1()) + .on_click(cx.listener(|this, _, _, cx| { + this.go_to_welcome_page(cx); + })), + ) + } + }), ), ) .child( @@ -430,8 +437,8 @@ impl Onboarding { .into_any_element() } else { Button::new("sign_in", "Sign In") - .style(ButtonStyle::Outlined) .full_width() + .style(ButtonStyle::Outlined) .on_click(|_, window, cx| { let client = Client::global(cx); window From f3f2dba606229a9c9dd3af728b9ed430fbcf9548 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Aug 2025 11:22:49 -0700 Subject: [PATCH 17/46] Minor stylistic cleanup in Windows platform (#35503) This PR doesn't change any logic, it just cleans up some naming and style in the windows platform layer. * Rename `WindowsWindowStatePtr` to `WindowsWindowInner`, since it isn't a pointer type. * Move window event handler methods into an impl on this type, so that all of the `state_ptr: &Rc` parameters can just be replaced with `&self`. * In window creation, use a `match` instead of a conditional followed by an unwrap There's a lot of whitespace in the diff, so view it with `w=1`. Release Notes: - N/A --- crates/gpui/src/platform/windows/events.rs | 2409 +++++++++--------- crates/gpui/src/platform/windows/platform.rs | 6 +- crates/gpui/src/platform/windows/window.rs | 127 +- 3 files changed, 1233 insertions(+), 1309 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 61f410a8c6..00b22fa807 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -28,997 +28,863 @@ pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5; const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1; -pub(crate) fn handle_msg( - handle: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> LRESULT { - let handled = match msg { - WM_ACTIVATE => handle_activate_msg(wparam, state_ptr), - WM_CREATE => handle_create_msg(handle, state_ptr), - WM_DEVICECHANGE => handle_device_change_msg(handle, wparam, state_ptr), - WM_MOVE => handle_move_msg(handle, lparam, state_ptr), - WM_SIZE => handle_size_msg(wparam, lparam, state_ptr), - WM_GETMINMAXINFO => handle_get_min_max_info_msg(lparam, state_ptr), - WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => handle_size_move_loop(handle), - WM_EXITSIZEMOVE | WM_EXITMENULOOP => handle_size_move_loop_exit(handle), - WM_TIMER => handle_timer_msg(handle, wparam, state_ptr), - WM_NCCALCSIZE => handle_calc_client_size(handle, wparam, lparam, state_ptr), - WM_DPICHANGED => handle_dpi_changed_msg(handle, wparam, lparam, state_ptr), - WM_DISPLAYCHANGE => handle_display_change_msg(handle, state_ptr), - WM_NCHITTEST => handle_hit_test_msg(handle, msg, wparam, lparam, state_ptr), - WM_PAINT => handle_paint_msg(handle, state_ptr), - WM_CLOSE => handle_close_msg(state_ptr), - WM_DESTROY => handle_destroy_msg(handle, state_ptr), - WM_MOUSEMOVE => handle_mouse_move_msg(handle, lparam, wparam, state_ptr), - WM_MOUSELEAVE | WM_NCMOUSELEAVE => handle_mouse_leave_msg(state_ptr), - WM_NCMOUSEMOVE => handle_nc_mouse_move_msg(handle, lparam, state_ptr), - WM_NCLBUTTONDOWN => { - handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam, state_ptr) +impl WindowsWindowInner { + pub(crate) fn handle_msg( + self: &Rc, + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + let handled = match msg { + WM_ACTIVATE => self.handle_activate_msg(wparam), + WM_CREATE => self.handle_create_msg(handle), + WM_DEVICECHANGE => self.handle_device_change_msg(handle, wparam), + WM_MOVE => self.handle_move_msg(handle, lparam), + WM_SIZE => self.handle_size_msg(wparam, lparam), + WM_GETMINMAXINFO => self.handle_get_min_max_info_msg(lparam), + WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => self.handle_size_move_loop(handle), + WM_EXITSIZEMOVE | WM_EXITMENULOOP => self.handle_size_move_loop_exit(handle), + WM_TIMER => self.handle_timer_msg(handle, wparam), + WM_NCCALCSIZE => self.handle_calc_client_size(handle, wparam, lparam), + WM_DPICHANGED => self.handle_dpi_changed_msg(handle, wparam, lparam), + WM_DISPLAYCHANGE => self.handle_display_change_msg(handle), + WM_NCHITTEST => self.handle_hit_test_msg(handle, msg, wparam, lparam), + WM_PAINT => self.handle_paint_msg(handle), + WM_CLOSE => self.handle_close_msg(), + WM_DESTROY => self.handle_destroy_msg(handle), + WM_MOUSEMOVE => self.handle_mouse_move_msg(handle, lparam, wparam), + WM_MOUSELEAVE | WM_NCMOUSELEAVE => self.handle_mouse_leave_msg(), + WM_NCMOUSEMOVE => self.handle_nc_mouse_move_msg(handle, lparam), + WM_NCLBUTTONDOWN => { + self.handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam) + } + WM_NCRBUTTONDOWN => { + self.handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam) + } + WM_NCMBUTTONDOWN => { + self.handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam) + } + WM_NCLBUTTONUP => { + self.handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam) + } + WM_NCRBUTTONUP => { + self.handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam) + } + WM_NCMBUTTONUP => { + self.handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam) + } + WM_LBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Left, lparam), + WM_RBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Right, lparam), + WM_MBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Middle, lparam), + WM_XBUTTONDOWN => { + self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_down_msg) + } + WM_LBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Left, lparam), + WM_RBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Right, lparam), + WM_MBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Middle, lparam), + WM_XBUTTONUP => { + self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_up_msg) + } + WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(handle, wparam, lparam), + WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(handle, wparam, lparam), + WM_SYSKEYDOWN => self.handle_syskeydown_msg(handle, wparam, lparam), + WM_SYSKEYUP => self.handle_syskeyup_msg(handle, wparam, lparam), + WM_SYSCOMMAND => self.handle_system_command(wparam), + WM_KEYDOWN => self.handle_keydown_msg(handle, wparam, lparam), + WM_KEYUP => self.handle_keyup_msg(handle, wparam, lparam), + WM_CHAR => self.handle_char_msg(wparam), + WM_DEADCHAR => self.handle_dead_char_msg(wparam), + WM_IME_STARTCOMPOSITION => self.handle_ime_position(handle), + WM_IME_COMPOSITION => self.handle_ime_composition(handle, lparam), + WM_SETCURSOR => self.handle_set_cursor(handle, lparam), + WM_SETTINGCHANGE => self.handle_system_settings_changed(handle, wparam, lparam), + WM_INPUTLANGCHANGE => self.handle_input_language_changed(lparam), + WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam), + WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), + _ => None, + }; + if let Some(n) = handled { + LRESULT(n) + } else { + unsafe { DefWindowProcW(handle, msg, wparam, lparam) } } - WM_NCRBUTTONDOWN => { - handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam, state_ptr) - } - WM_NCMBUTTONDOWN => { - handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr) - } - WM_NCLBUTTONUP => { - handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam, state_ptr) - } - WM_NCRBUTTONUP => { - handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam, state_ptr) - } - WM_NCMBUTTONUP => { - handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr) - } - WM_LBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Left, lparam, state_ptr), - WM_RBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Right, lparam, state_ptr), - WM_MBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Middle, lparam, state_ptr), - WM_XBUTTONDOWN => { - handle_xbutton_msg(handle, wparam, lparam, handle_mouse_down_msg, state_ptr) - } - WM_LBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Left, lparam, state_ptr), - WM_RBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Right, lparam, state_ptr), - WM_MBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Middle, lparam, state_ptr), - WM_XBUTTONUP => handle_xbutton_msg(handle, wparam, lparam, handle_mouse_up_msg, state_ptr), - WM_MOUSEWHEEL => handle_mouse_wheel_msg(handle, wparam, lparam, state_ptr), - WM_MOUSEHWHEEL => handle_mouse_horizontal_wheel_msg(handle, wparam, lparam, state_ptr), - WM_SYSKEYDOWN => handle_syskeydown_msg(handle, wparam, lparam, state_ptr), - WM_SYSKEYUP => handle_syskeyup_msg(handle, wparam, lparam, state_ptr), - WM_SYSCOMMAND => handle_system_command(wparam, state_ptr), - WM_KEYDOWN => handle_keydown_msg(handle, wparam, lparam, state_ptr), - WM_KEYUP => handle_keyup_msg(handle, wparam, lparam, state_ptr), - WM_CHAR => handle_char_msg(wparam, state_ptr), - WM_DEADCHAR => handle_dead_char_msg(wparam, state_ptr), - WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr), - WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr), - WM_SETCURSOR => handle_set_cursor(handle, lparam, state_ptr), - WM_SETTINGCHANGE => handle_system_settings_changed(handle, wparam, lparam, state_ptr), - WM_INPUTLANGCHANGE => handle_input_language_changed(lparam, state_ptr), - WM_GPUI_CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr), - WM_GPUI_FORCE_UPDATE_WINDOW => draw_window(handle, true, state_ptr), - _ => None, - }; - if let Some(n) = handled { - LRESULT(n) - } else { - unsafe { DefWindowProcW(handle, msg, wparam, lparam) } - } -} - -fn handle_move_msg( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let origin = logical_point( - lparam.signed_loword() as f32, - lparam.signed_hiword() as f32, - lock.scale_factor, - ); - lock.origin = origin; - let size = lock.logical_size; - let center_x = origin.x.0 + size.width.0 / 2.; - let center_y = origin.y.0 + size.height.0 / 2.; - let monitor_bounds = lock.display.bounds(); - if center_x < monitor_bounds.left().0 - || center_x > monitor_bounds.right().0 - || center_y < monitor_bounds.top().0 - || center_y > monitor_bounds.bottom().0 - { - // center of the window may have moved to another monitor - let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; - // minimize the window can trigger this event too, in this case, - // monitor is invalid, we do nothing. - if !monitor.is_invalid() && lock.display.handle != monitor { - // we will get the same monitor if we only have one - lock.display = WindowsDisplay::new_with_handle(monitor); - } - } - if let Some(mut callback) = lock.callbacks.moved.take() { - drop(lock); - callback(); - state_ptr.state.borrow_mut().callbacks.moved = Some(callback); - } - Some(0) -} - -fn handle_get_min_max_info_msg( - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let lock = state_ptr.state.borrow(); - let min_size = lock.min_size?; - let scale_factor = lock.scale_factor; - let boarder_offset = lock.border_offset; - drop(lock); - unsafe { - let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO); - minmax_info.ptMinTrackSize.x = - min_size.width.scale(scale_factor).0 as i32 + boarder_offset.width_offset; - minmax_info.ptMinTrackSize.y = - min_size.height.scale(scale_factor).0 as i32 + boarder_offset.height_offset; - } - Some(0) -} - -fn handle_size_msg( - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - - // Don't resize the renderer when the window is minimized, but record that it was minimized so - // that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`. - if wparam.0 == SIZE_MINIMIZED as usize { - lock.restore_from_minimized = lock.callbacks.request_frame.take(); - return Some(0); } - let width = lparam.loword().max(1) as i32; - let height = lparam.hiword().max(1) as i32; - let new_size = size(DevicePixels(width), DevicePixels(height)); - let scale_factor = lock.scale_factor; - if lock.restore_from_minimized.is_some() { - lock.callbacks.request_frame = lock.restore_from_minimized.take(); - } else { - lock.renderer.resize(new_size).log_err(); - } - let new_size = new_size.to_pixels(scale_factor); - lock.logical_size = new_size; - if let Some(mut callback) = lock.callbacks.resize.take() { - drop(lock); - callback(new_size, scale_factor); - state_ptr.state.borrow_mut().callbacks.resize = Some(callback); - } - Some(0) -} - -fn handle_size_move_loop(handle: HWND) -> Option { - unsafe { - let ret = SetTimer( - Some(handle), - SIZE_MOVE_LOOP_TIMER_ID, - USER_TIMER_MINIMUM, - None, + fn handle_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let origin = logical_point( + lparam.signed_loword() as f32, + lparam.signed_hiword() as f32, + lock.scale_factor, ); - if ret == 0 { - log::error!( - "unable to create timer: {}", - std::io::Error::last_os_error() - ); - } - } - None -} - -fn handle_size_move_loop_exit(handle: HWND) -> Option { - unsafe { - KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err(); - } - None -} - -fn handle_timer_msg( - handle: HWND, - wparam: WPARAM, - state_ptr: Rc, -) -> Option { - if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { - for runnable in state_ptr.main_receiver.drain() { - runnable.run(); - } - handle_paint_msg(handle, state_ptr) - } else { - None - } -} - -fn handle_paint_msg(handle: HWND, state_ptr: Rc) -> Option { - draw_window(handle, false, state_ptr) -} - -fn handle_close_msg(state_ptr: Rc) -> Option { - let mut callback = state_ptr.state.borrow_mut().callbacks.should_close.take()?; - let should_close = callback(); - state_ptr.state.borrow_mut().callbacks.should_close = Some(callback); - if should_close { None } else { Some(0) } -} - -fn handle_destroy_msg(handle: HWND, state_ptr: Rc) -> Option { - let callback = { - let mut lock = state_ptr.state.borrow_mut(); - lock.callbacks.close.take() - }; - if let Some(callback) = callback { - callback(); - } - unsafe { - PostThreadMessageW( - state_ptr.main_thread_id_win32, - WM_GPUI_CLOSE_ONE_WINDOW, - WPARAM(state_ptr.validation_number), - LPARAM(handle.0 as isize), - ) - .log_err(); - } - Some(0) -} - -fn handle_mouse_move_msg( - handle: HWND, - lparam: LPARAM, - wparam: WPARAM, - state_ptr: Rc, -) -> Option { - start_tracking_mouse(handle, &state_ptr, TME_LEAVE); - - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - drop(lock); - - let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) { - flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left), - flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right), - flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle), - flags if flags.contains(MK_XBUTTON1) => { - Some(MouseButton::Navigate(NavigationDirection::Back)) - } - flags if flags.contains(MK_XBUTTON2) => { - Some(MouseButton::Navigate(NavigationDirection::Forward)) - } - _ => None, - }; - let x = lparam.signed_loword() as f32; - let y = lparam.signed_hiword() as f32; - let input = PlatformInput::MouseMove(MouseMoveEvent { - position: logical_point(x, y, scale_factor), - pressed_button, - modifiers: current_modifiers(), - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_mouse_leave_msg(state_ptr: Rc) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - lock.hovered = false; - if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { - drop(lock); - callback(false); - state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback); - } - - Some(0) -} - -fn handle_syskeydown_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held: lparam.0 & (0x1 << 30) > 0, - }) - })?; - let mut func = lock.callbacks.input.take()?; - drop(lock); - - let handled = !func(input).propagate; - - let mut lock = state_ptr.state.borrow_mut(); - lock.callbacks.input = Some(func); - - if handled { - lock.system_key_handled = true; - Some(0) - } else { - // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}` - // shortcuts. - None - } -} - -fn handle_syskeyup_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyUp(KeyUpEvent { keystroke }) - })?; - let mut func = lock.callbacks.input.take()?; - drop(lock); - func(input); - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - // Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event. - Some(0) -} - -// It's a known bug that you can't trigger `ctrl-shift-0`. See: -// https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers -fn handle_keydown_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held: lparam.0 & (0x1 << 30) > 0, - }) - }) else { - return Some(1); - }; - drop(lock); - - let is_composing = with_input_handler(&state_ptr, |input_handler| { - input_handler.marked_text_range() - }) - .flatten() - .is_some(); - if is_composing { - translate_message(handle, wparam, lparam); - return Some(0); - } - - let Some(mut func) = state_ptr.state.borrow_mut().callbacks.input.take() else { - return Some(1); - }; - - let handled = !func(input).propagate; - - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { - Some(0) - } else { - translate_message(handle, wparam, lparam); - Some(1) - } -} - -fn handle_keyup_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyUp(KeyUpEvent { keystroke }) - }) else { - return Some(1); - }; - - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - drop(lock); - - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_char_msg(wparam: WPARAM, state_ptr: Rc) -> Option { - let input = parse_char_message(wparam, &state_ptr)?; - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_text_in_range(None, &input); - }); - - Some(0) -} - -fn handle_dead_char_msg(wparam: WPARAM, state_ptr: Rc) -> Option { - let ch = char::from_u32(wparam.0 as u32)?.to_string(); - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_and_mark_text_in_range(None, &ch, None); - }); - None -} - -fn handle_mouse_down_msg( - handle: HWND, - button: MouseButton, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - unsafe { SetCapture(handle) }; - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let x = lparam.signed_loword(); - let y = lparam.signed_hiword(); - let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32)); - let click_count = lock.click_state.update(button, physical_point); - let scale_factor = lock.scale_factor; - drop(lock); - - let input = PlatformInput::MouseDown(MouseDownEvent { - button, - position: logical_point(x as f32, y as f32, scale_factor), - modifiers: current_modifiers(), - click_count, - first_mouse: false, - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_mouse_up_msg( - _handle: HWND, - button: MouseButton, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - unsafe { ReleaseCapture().log_err() }; - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let x = lparam.signed_loword() as f32; - let y = lparam.signed_hiword() as f32; - let click_count = lock.click_state.current_count; - let scale_factor = lock.scale_factor; - drop(lock); - - let input = PlatformInput::MouseUp(MouseUpEvent { - button, - position: logical_point(x, y, scale_factor), - modifiers: current_modifiers(), - click_count, - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_xbutton_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - handler: impl Fn(HWND, MouseButton, LPARAM, Rc) -> Option, - state_ptr: Rc, -) -> Option { - let nav_dir = match wparam.hiword() { - XBUTTON1 => NavigationDirection::Back, - XBUTTON2 => NavigationDirection::Forward, - _ => return Some(1), - }; - handler(handle, MouseButton::Navigate(nav_dir), lparam, state_ptr) -} - -fn handle_mouse_wheel_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let modifiers = current_modifiers(); - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - let wheel_scroll_amount = match modifiers.shift { - true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars, - false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines, - }; - drop(lock); - - let wheel_distance = - (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - delta: ScrollDelta::Lines(match modifiers.shift { - true => Point { - x: wheel_distance, - y: 0.0, - }, - false => Point { - y: wheel_distance, - x: 0.0, - }, - }), - modifiers, - touch_phase: TouchPhase::Moved, - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_mouse_horizontal_wheel_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars; - drop(lock); - - let wheel_distance = - (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let event = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - delta: ScrollDelta::Lines(Point { - x: wheel_distance, - y: 0.0, - }), - modifiers: current_modifiers(), - touch_phase: TouchPhase::Moved, - }); - let handled = !func(event).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn retrieve_caret_position(state_ptr: &Rc) -> Option { - with_input_handler_and_scale_factor(state_ptr, |input_handler, scale_factor| { - let caret_range = input_handler.selected_text_range(false)?; - let caret_position = input_handler.bounds_for_range(caret_range.range)?; - Some(POINT { - // logical to physical - x: (caret_position.origin.x.0 * scale_factor) as i32, - y: (caret_position.origin.y.0 * scale_factor) as i32 - + ((caret_position.size.height.0 * scale_factor) as i32 / 2), - }) - }) -} - -fn handle_ime_position(handle: HWND, state_ptr: Rc) -> Option { - unsafe { - let ctx = ImmGetContext(handle); - - let Some(caret_position) = retrieve_caret_position(&state_ptr) else { - return Some(0); - }; + lock.origin = origin; + let size = lock.logical_size; + let center_x = origin.x.0 + size.width.0 / 2.; + let center_y = origin.y.0 + size.height.0 / 2.; + let monitor_bounds = lock.display.bounds(); + if center_x < monitor_bounds.left().0 + || center_x > monitor_bounds.right().0 + || center_y < monitor_bounds.top().0 + || center_y > monitor_bounds.bottom().0 { - let config = COMPOSITIONFORM { - dwStyle: CFS_POINT, - ptCurrentPos: caret_position, - ..Default::default() - }; - ImmSetCompositionWindow(ctx, &config as _).ok().log_err(); - } - { - let config = CANDIDATEFORM { - dwStyle: CFS_CANDIDATEPOS, - ptCurrentPos: caret_position, - ..Default::default() - }; - ImmSetCandidateWindow(ctx, &config as _).ok().log_err(); - } - ImmReleaseContext(handle, ctx).ok().log_err(); - Some(0) - } -} - -fn handle_ime_composition( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let ctx = unsafe { ImmGetContext(handle) }; - let result = handle_ime_composition_inner(ctx, lparam, state_ptr); - unsafe { ImmReleaseContext(handle, ctx).ok().log_err() }; - result -} - -fn handle_ime_composition_inner( - ctx: HIMC, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let lparam = lparam.0 as u32; - if lparam == 0 { - // Japanese IME may send this message with lparam = 0, which indicates that - // there is no composition string. - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_text_in_range(None, ""); - })?; - Some(0) - } else { - if lparam & GCS_COMPSTR.0 > 0 { - let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?; - let caret_pos = (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| { - let pos = retrieve_composition_cursor_position(ctx); - pos..pos - }); - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos); - })?; - } - if lparam & GCS_RESULTSTR.0 > 0 { - let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_text_in_range(None, &comp_result); - })?; - return Some(0); - } - - // currently, we don't care other stuff - None - } -} - -/// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize -fn handle_calc_client_size( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - if !state_ptr.hide_title_bar || state_ptr.state.borrow().is_fullscreen() || wparam.0 == 0 { - return None; - } - - let is_maximized = state_ptr.state.borrow().is_maximized(); - let insets = get_client_area_insets(handle, is_maximized, state_ptr.windows_version); - // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure - let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS; - let mut requested_client_rect = unsafe { &mut ((*params).rgrc) }; - - requested_client_rect[0].left += insets.left; - requested_client_rect[0].top += insets.top; - requested_client_rect[0].right -= insets.right; - requested_client_rect[0].bottom -= insets.bottom; - - // Fix auto hide taskbar not showing. This solution is based on the approach - // used by Chrome. However, it may result in one row of pixels being obscured - // in our client area. But as Chrome says, "there seems to be no better solution." - if is_maximized { - if let Some(ref taskbar_position) = state_ptr - .state - .borrow() - .system_settings - .auto_hide_taskbar_position - { - // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge, - // so the window isn't treated as a "fullscreen app", which would cause - // the taskbar to disappear. - match taskbar_position { - AutoHideTaskbarPosition::Left => { - requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX - } - AutoHideTaskbarPosition::Top => { - requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX - } - AutoHideTaskbarPosition::Right => { - requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX - } - AutoHideTaskbarPosition::Bottom => { - requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX - } + // center of the window may have moved to another monitor + let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // minimize the window can trigger this event too, in this case, + // monitor is invalid, we do nothing. + if !monitor.is_invalid() && lock.display.handle != monitor { + // we will get the same monitor if we only have one + lock.display = WindowsDisplay::new_with_handle(monitor); } } - } - - Some(0) -} - -fn handle_activate_msg(wparam: WPARAM, state_ptr: Rc) -> Option { - let activated = wparam.loword() > 0; - let this = state_ptr.clone(); - state_ptr - .executor - .spawn(async move { - let mut lock = this.state.borrow_mut(); - if let Some(mut func) = lock.callbacks.active_status_change.take() { - drop(lock); - func(activated); - this.state.borrow_mut().callbacks.active_status_change = Some(func); - } - }) - .detach(); - - None -} - -fn handle_create_msg(handle: HWND, state_ptr: Rc) -> Option { - if state_ptr.hide_title_bar { - notify_frame_changed(handle); - Some(0) - } else { - None - } -} - -fn handle_dpi_changed_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let new_dpi = wparam.loword() as f32; - let mut lock = state_ptr.state.borrow_mut(); - lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; - lock.border_offset.update(handle).log_err(); - drop(lock); - - let rect = unsafe { &*(lparam.0 as *const RECT) }; - let width = rect.right - rect.left; - let height = rect.bottom - rect.top; - // this will emit `WM_SIZE` and `WM_MOVE` right here - // even before this function returns - // the new size is handled in `WM_SIZE` - unsafe { - SetWindowPos( - handle, - None, - rect.left, - rect.top, - width, - height, - SWP_NOZORDER | SWP_NOACTIVATE, - ) - .context("unable to set window position after dpi has changed") - .log_err(); - } - - Some(0) -} - -/// The following conditions will trigger this event: -/// 1. The monitor on which the window is located goes offline or changes resolution. -/// 2. Another monitor goes offline, is plugged in, or changes resolution. -/// -/// In either case, the window will only receive information from the monitor on which -/// it is located. -/// -/// For example, in the case of condition 2, where the monitor on which the window is -/// located has actually changed nothing, it will still receive this event. -fn handle_display_change_msg(handle: HWND, state_ptr: Rc) -> Option { - // NOTE: - // Even the `lParam` holds the resolution of the screen, we just ignore it. - // Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize - // are handled there. - // So we only care about if monitor is disconnected. - let previous_monitor = state_ptr.state.borrow().display; - if WindowsDisplay::is_connected(previous_monitor.handle) { - // we are fine, other display changed - return None; - } - // display disconnected - // in this case, the OS will move our window to another monitor, and minimize it. - // we deminimize the window and query the monitor after moving - unsafe { - let _ = ShowWindow(handle, SW_SHOWNORMAL); - }; - let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; - // all monitors disconnected - if new_monitor.is_invalid() { - log::error!("No monitor detected!"); - return None; - } - let new_display = WindowsDisplay::new_with_handle(new_monitor); - state_ptr.state.borrow_mut().display = new_display; - Some(0) -} - -fn handle_hit_test_msg( - handle: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - if !state_ptr.is_movable || state_ptr.state.borrow().is_fullscreen() { - return None; - } - - let mut lock = state_ptr.state.borrow_mut(); - if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() { - drop(lock); - let area = callback(); - state_ptr - .state - .borrow_mut() - .callbacks - .hit_test_window_control = Some(callback); - if let Some(area) = area { - return match area { - WindowControlArea::Drag => Some(HTCAPTION as _), - WindowControlArea::Close => Some(HTCLOSE as _), - WindowControlArea::Max => Some(HTMAXBUTTON as _), - WindowControlArea::Min => Some(HTMINBUTTON as _), - }; + if let Some(mut callback) = lock.callbacks.moved.take() { + drop(lock); + callback(); + self.state.borrow_mut().callbacks.moved = Some(callback); } - } else { - drop(lock); + Some(0) } - if !state_ptr.hide_title_bar { - // If the OS draws the title bar, we don't need to handle hit test messages. - return None; - } - - // default handler for resize areas - let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) }; - if matches!( - hit.0 as u32, - HTNOWHERE - | HTRIGHT - | HTLEFT - | HTTOPLEFT - | HTTOP - | HTTOPRIGHT - | HTBOTTOMRIGHT - | HTBOTTOM - | HTBOTTOMLEFT - ) { - return Some(hit.0); - } - - if state_ptr.state.borrow().is_fullscreen() { - return Some(HTCLIENT as _); - } - - let dpi = unsafe { GetDpiForWindow(handle) }; - let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) }; - - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - if !state_ptr.state.borrow().is_maximized() && cursor_point.y >= 0 && cursor_point.y <= frame_y - { - return Some(HTTOP as _); - } - - Some(HTCLIENT as _) -} - -fn handle_nc_mouse_move_msg( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - start_tracking_mouse(handle, &state_ptr, TME_LEAVE | TME_NONCLIENT); - - let mut lock = state_ptr.state.borrow_mut(); - let mut func = lock.callbacks.input.take()?; - let scale_factor = lock.scale_factor; - drop(lock); - - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::MouseMove(MouseMoveEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - pressed_button: None, - modifiers: current_modifiers(), - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { None } -} - -fn handle_nc_mouse_down_msg( - handle: HWND, - button: MouseButton, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - if let Some(mut func) = lock.callbacks.input.take() { + fn handle_get_min_max_info_msg(&self, lparam: LPARAM) -> Option { + let lock = self.state.borrow(); + let min_size = lock.min_size?; let scale_factor = lock.scale_factor; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), + let boarder_offset = lock.border_offset; + drop(lock); + unsafe { + let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO); + minmax_info.ptMinTrackSize.x = + min_size.width.scale(scale_factor).0 as i32 + boarder_offset.width_offset; + minmax_info.ptMinTrackSize.y = + min_size.height.scale(scale_factor).0 as i32 + boarder_offset.height_offset; + } + Some(0) + } + + fn handle_size_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + + // Don't resize the renderer when the window is minimized, but record that it was minimized so + // that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`. + if wparam.0 == SIZE_MINIMIZED as usize { + lock.restore_from_minimized = lock.callbacks.request_frame.take(); + return Some(0); + } + + let width = lparam.loword().max(1) as i32; + let height = lparam.hiword().max(1) as i32; + let new_size = size(DevicePixels(width), DevicePixels(height)); + let scale_factor = lock.scale_factor; + if lock.restore_from_minimized.is_some() { + lock.callbacks.request_frame = lock.restore_from_minimized.take(); + } else { + lock.renderer.resize(new_size).log_err(); + } + let new_size = new_size.to_pixels(scale_factor); + lock.logical_size = new_size; + if let Some(mut callback) = lock.callbacks.resize.take() { + drop(lock); + callback(new_size, scale_factor); + self.state.borrow_mut().callbacks.resize = Some(callback); + } + Some(0) + } + + fn handle_size_move_loop(&self, handle: HWND) -> Option { + unsafe { + let ret = SetTimer( + Some(handle), + SIZE_MOVE_LOOP_TIMER_ID, + USER_TIMER_MINIMUM, + None, + ); + if ret == 0 { + log::error!( + "unable to create timer: {}", + std::io::Error::last_os_error() + ); + } + } + None + } + + fn handle_size_move_loop_exit(&self, handle: HWND) -> Option { + unsafe { + KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err(); + } + None + } + + fn handle_timer_msg(&self, handle: HWND, wparam: WPARAM) -> Option { + if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { + for runnable in self.main_receiver.drain() { + runnable.run(); + } + self.handle_paint_msg(handle) + } else { + None + } + } + + fn handle_paint_msg(&self, handle: HWND) -> Option { + self.draw_window(handle, false) + } + + fn handle_close_msg(&self) -> Option { + let mut callback = self.state.borrow_mut().callbacks.should_close.take()?; + let should_close = callback(); + self.state.borrow_mut().callbacks.should_close = Some(callback); + if should_close { None } else { Some(0) } + } + + fn handle_destroy_msg(&self, handle: HWND) -> Option { + let callback = { + let mut lock = self.state.borrow_mut(); + lock.callbacks.close.take() }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y)); + if let Some(callback) = callback { + callback(); + } + unsafe { + PostThreadMessageW( + self.main_thread_id_win32, + WM_GPUI_CLOSE_ONE_WINDOW, + WPARAM(self.validation_number), + LPARAM(handle.0 as isize), + ) + .log_err(); + } + Some(0) + } + + fn handle_mouse_move_msg(&self, handle: HWND, lparam: LPARAM, wparam: WPARAM) -> Option { + self.start_tracking_mouse(handle, TME_LEAVE); + + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + drop(lock); + + let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) { + flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left), + flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right), + flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle), + flags if flags.contains(MK_XBUTTON1) => { + Some(MouseButton::Navigate(NavigationDirection::Back)) + } + flags if flags.contains(MK_XBUTTON2) => { + Some(MouseButton::Navigate(NavigationDirection::Forward)) + } + _ => None, + }; + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: logical_point(x, y, scale_factor), + pressed_button, + modifiers: current_modifiers(), + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_mouse_leave_msg(&self) -> Option { + let mut lock = self.state.borrow_mut(); + lock.hovered = false; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(false); + self.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + + Some(0) + } + + fn handle_syskeydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }) + })?; + let mut func = lock.callbacks.input.take()?; + drop(lock); + + let handled = !func(input).propagate; + + let mut lock = self.state.borrow_mut(); + lock.callbacks.input = Some(func); + + if handled { + lock.system_key_handled = true; + Some(0) + } else { + // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}` + // shortcuts. + None + } + } + + fn handle_syskeyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyUp(KeyUpEvent { keystroke }) + })?; + let mut func = lock.callbacks.input.take()?; + drop(lock); + func(input); + self.state.borrow_mut().callbacks.input = Some(func); + + // Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event. + Some(0) + } + + // It's a known bug that you can't trigger `ctrl-shift-0`. See: + // https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers + fn handle_keydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }) + }) else { + return Some(1); + }; + drop(lock); + + let is_composing = self + .with_input_handler(|input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + if is_composing { + translate_message(handle, wparam, lparam); + return Some(0); + } + + let Some(mut func) = self.state.borrow_mut().callbacks.input.take() else { + return Some(1); + }; + + let handled = !func(input).propagate; + + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { + Some(0) + } else { + translate_message(handle, wparam, lparam); + Some(1) + } + } + + fn handle_keyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyUp(KeyUpEvent { keystroke }) + }) else { + return Some(1); + }; + + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + drop(lock); + + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_char_msg(&self, wparam: WPARAM) -> Option { + let input = self.parse_char_message(wparam)?; + self.with_input_handler(|input_handler| { + input_handler.replace_text_in_range(None, &input); + }); + + Some(0) + } + + fn handle_dead_char_msg(&self, wparam: WPARAM) -> Option { + let ch = char::from_u32(wparam.0 as u32)?.to_string(); + self.with_input_handler(|input_handler| { + input_handler.replace_and_mark_text_in_range(None, &ch, None); + }); + None + } + + fn handle_mouse_down_msg( + &self, + handle: HWND, + button: MouseButton, + lparam: LPARAM, + ) -> Option { + unsafe { SetCapture(handle) }; + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let x = lparam.signed_loword(); + let y = lparam.signed_hiword(); + let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32)); let click_count = lock.click_state.update(button, physical_point); + let scale_factor = lock.scale_factor; drop(lock); let input = PlatformInput::MouseDown(MouseDownEvent { button, - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + position: logical_point(x as f32, y as f32, scale_factor), modifiers: current_modifiers(), click_count, first_mouse: false, }); - let result = func(input.clone()); - let handled = !result.propagate || result.default_prevented; - state_ptr.state.borrow_mut().callbacks.input = Some(func); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); - if handled { - return Some(0); - } - } else { - drop(lock); - }; + if handled { Some(0) } else { Some(1) } + } - // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc - if button == MouseButton::Left { - match wparam.0 as u32 { - HTMINBUTTON => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTMINBUTTON), - HTMAXBUTTON => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTMAXBUTTON), - HTCLOSE => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTCLOSE), - _ => return None, + fn handle_mouse_up_msg( + &self, + _handle: HWND, + button: MouseButton, + lparam: LPARAM, + ) -> Option { + unsafe { ReleaseCapture().log_err() }; + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); }; + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let click_count = lock.click_state.current_count; + let scale_factor = lock.scale_factor; + drop(lock); + + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: logical_point(x, y, scale_factor), + modifiers: current_modifiers(), + click_count, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_xbutton_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + handler: impl Fn(&Self, HWND, MouseButton, LPARAM) -> Option, + ) -> Option { + let nav_dir = match wparam.hiword() { + XBUTTON1 => NavigationDirection::Back, + XBUTTON2 => NavigationDirection::Forward, + _ => return Some(1), + }; + handler(self, handle, MouseButton::Navigate(nav_dir), lparam) + } + + fn handle_mouse_wheel_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let modifiers = current_modifiers(); + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + let wheel_scroll_amount = match modifiers.shift { + true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars, + false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines, + }; + drop(lock); + + let wheel_distance = + (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(match modifiers.shift { + true => Point { + x: wheel_distance, + y: 0.0, + }, + false => Point { + y: wheel_distance, + x: 0.0, + }, + }), + modifiers, + touch_phase: TouchPhase::Moved, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_mouse_horizontal_wheel_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars; + drop(lock); + + let wheel_distance = + (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let event = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(Point { + x: wheel_distance, + y: 0.0, + }), + modifiers: current_modifiers(), + touch_phase: TouchPhase::Moved, + }); + let handled = !func(event).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn retrieve_caret_position(&self) -> Option { + self.with_input_handler_and_scale_factor(|input_handler, scale_factor| { + let caret_range = input_handler.selected_text_range(false)?; + let caret_position = input_handler.bounds_for_range(caret_range.range)?; + Some(POINT { + // logical to physical + x: (caret_position.origin.x.0 * scale_factor) as i32, + y: (caret_position.origin.y.0 * scale_factor) as i32 + + ((caret_position.size.height.0 * scale_factor) as i32 / 2), + }) + }) + } + + fn handle_ime_position(&self, handle: HWND) -> Option { + unsafe { + let ctx = ImmGetContext(handle); + + let Some(caret_position) = self.retrieve_caret_position() else { + return Some(0); + }; + { + let config = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: caret_position, + ..Default::default() + }; + ImmSetCompositionWindow(ctx, &config as _).ok().log_err(); + } + { + let config = CANDIDATEFORM { + dwStyle: CFS_CANDIDATEPOS, + ptCurrentPos: caret_position, + ..Default::default() + }; + ImmSetCandidateWindow(ctx, &config as _).ok().log_err(); + } + ImmReleaseContext(handle, ctx).ok().log_err(); + Some(0) + } + } + + fn handle_ime_composition(&self, handle: HWND, lparam: LPARAM) -> Option { + let ctx = unsafe { ImmGetContext(handle) }; + let result = self.handle_ime_composition_inner(ctx, lparam); + unsafe { ImmReleaseContext(handle, ctx).ok().log_err() }; + result + } + + fn handle_ime_composition_inner(&self, ctx: HIMC, lparam: LPARAM) -> Option { + let lparam = lparam.0 as u32; + if lparam == 0 { + // Japanese IME may send this message with lparam = 0, which indicates that + // there is no composition string. + self.with_input_handler(|input_handler| { + input_handler.replace_text_in_range(None, ""); + })?; + Some(0) + } else { + if lparam & GCS_COMPSTR.0 > 0 { + let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?; + let caret_pos = + (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| { + let pos = retrieve_composition_cursor_position(ctx); + pos..pos + }); + self.with_input_handler(|input_handler| { + input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos); + })?; + } + if lparam & GCS_RESULTSTR.0 > 0 { + let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; + self.with_input_handler(|input_handler| { + input_handler.replace_text_in_range(None, &comp_result); + })?; + return Some(0); + } + + // currently, we don't care other stuff + None + } + } + + /// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize + fn handle_calc_client_size( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + if !self.hide_title_bar || self.state.borrow().is_fullscreen() || wparam.0 == 0 { + return None; + } + + let is_maximized = self.state.borrow().is_maximized(); + let insets = get_client_area_insets(handle, is_maximized, self.windows_version); + // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure + let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS; + let mut requested_client_rect = unsafe { &mut ((*params).rgrc) }; + + requested_client_rect[0].left += insets.left; + requested_client_rect[0].top += insets.top; + requested_client_rect[0].right -= insets.right; + requested_client_rect[0].bottom -= insets.bottom; + + // Fix auto hide taskbar not showing. This solution is based on the approach + // used by Chrome. However, it may result in one row of pixels being obscured + // in our client area. But as Chrome says, "there seems to be no better solution." + if is_maximized { + if let Some(ref taskbar_position) = self + .state + .borrow() + .system_settings + .auto_hide_taskbar_position + { + // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge, + // so the window isn't treated as a "fullscreen app", which would cause + // the taskbar to disappear. + match taskbar_position { + AutoHideTaskbarPosition::Left => { + requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Top => { + requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Right => { + requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Bottom => { + requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX + } + } + } + } + Some(0) - } else { + } + + fn handle_activate_msg(self: &Rc, wparam: WPARAM) -> Option { + let activated = wparam.loword() > 0; + let this = self.clone(); + self.executor + .spawn(async move { + let mut lock = this.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.active_status_change.take() { + drop(lock); + func(activated); + this.state.borrow_mut().callbacks.active_status_change = Some(func); + } + }) + .detach(); + None } -} -fn handle_nc_mouse_up_msg( - handle: HWND, - button: MouseButton, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - if let Some(mut func) = lock.callbacks.input.take() { + fn handle_create_msg(&self, handle: HWND) -> Option { + if self.hide_title_bar { + notify_frame_changed(handle); + Some(0) + } else { + None + } + } + + fn handle_dpi_changed_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let new_dpi = wparam.loword() as f32; + let mut lock = self.state.borrow_mut(); + lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; + lock.border_offset.update(handle).log_err(); + drop(lock); + + let rect = unsafe { &*(lparam.0 as *const RECT) }; + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + // this will emit `WM_SIZE` and `WM_MOVE` right here + // even before this function returns + // the new size is handled in `WM_SIZE` + unsafe { + SetWindowPos( + handle, + None, + rect.left, + rect.top, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE, + ) + .context("unable to set window position after dpi has changed") + .log_err(); + } + + Some(0) + } + + /// The following conditions will trigger this event: + /// 1. The monitor on which the window is located goes offline or changes resolution. + /// 2. Another monitor goes offline, is plugged in, or changes resolution. + /// + /// In either case, the window will only receive information from the monitor on which + /// it is located. + /// + /// For example, in the case of condition 2, where the monitor on which the window is + /// located has actually changed nothing, it will still receive this event. + fn handle_display_change_msg(&self, handle: HWND) -> Option { + // NOTE: + // Even the `lParam` holds the resolution of the screen, we just ignore it. + // Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize + // are handled there. + // So we only care about if monitor is disconnected. + let previous_monitor = self.state.borrow().display; + if WindowsDisplay::is_connected(previous_monitor.handle) { + // we are fine, other display changed + return None; + } + // display disconnected + // in this case, the OS will move our window to another monitor, and minimize it. + // we deminimize the window and query the monitor after moving + unsafe { + let _ = ShowWindow(handle, SW_SHOWNORMAL); + }; + let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // all monitors disconnected + if new_monitor.is_invalid() { + log::error!("No monitor detected!"); + return None; + } + let new_display = WindowsDisplay::new_with_handle(new_monitor); + self.state.borrow_mut().display = new_display; + Some(0) + } + + fn handle_hit_test_msg( + &self, + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + if !self.is_movable || self.state.borrow().is_fullscreen() { + return None; + } + + let mut lock = self.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() { + drop(lock); + let area = callback(); + self.state.borrow_mut().callbacks.hit_test_window_control = Some(callback); + if let Some(area) = area { + return match area { + WindowControlArea::Drag => Some(HTCAPTION as _), + WindowControlArea::Close => Some(HTCLOSE as _), + WindowControlArea::Max => Some(HTMAXBUTTON as _), + WindowControlArea::Min => Some(HTMINBUTTON as _), + }; + } + } else { + drop(lock); + } + + if !self.hide_title_bar { + // If the OS draws the title bar, we don't need to handle hit test messages. + return None; + } + + // default handler for resize areas + let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) }; + if matches!( + hit.0 as u32, + HTNOWHERE + | HTRIGHT + | HTLEFT + | HTTOPLEFT + | HTTOP + | HTTOPRIGHT + | HTBOTTOMRIGHT + | HTBOTTOM + | HTBOTTOMLEFT + ) { + return Some(hit.0); + } + + if self.state.borrow().is_fullscreen() { + return Some(HTCLIENT as _); + } + + let dpi = unsafe { GetDpiForWindow(handle) }; + let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) }; + + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + if !self.state.borrow().is_maximized() && cursor_point.y >= 0 && cursor_point.y <= frame_y { + return Some(HTTOP as _); + } + + Some(HTCLIENT as _) + } + + fn handle_nc_mouse_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { + self.start_tracking_mouse(handle, TME_LEAVE | TME_NONCLIENT); + + let mut lock = self.state.borrow_mut(); + let mut func = lock.callbacks.input.take()?; let scale_factor = lock.scale_factor; drop(lock); @@ -1027,253 +893,355 @@ fn handle_nc_mouse_up_msg( y: lparam.signed_hiword().into(), }; unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::MouseUp(MouseUpEvent { - button, + let input = PlatformInput::MouseMove(MouseMoveEvent { position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + pressed_button: None, modifiers: current_modifiers(), - click_count: 1, }); let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); + self.state.borrow_mut().callbacks.input = Some(func); - if handled { - return Some(0); - } - } else { - drop(lock); + if handled { Some(0) } else { None } } - let last_pressed = state_ptr.state.borrow_mut().nc_button_pressed.take(); - if button == MouseButton::Left - && let Some(last_pressed) = last_pressed - { - let handled = match (wparam.0 as u32, last_pressed) { - (HTMINBUTTON, HTMINBUTTON) => { - unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; - true + fn handle_nc_mouse_down_msg( + &self, + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let mut lock = self.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y)); + let click_count = lock.click_state.update(button, physical_point); + drop(lock); + + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + modifiers: current_modifiers(), + click_count, + first_mouse: false, + }); + let result = func(input.clone()); + let handled = !result.propagate || result.default_prevented; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { + return Some(0); } - (HTMAXBUTTON, HTMAXBUTTON) => { - if state_ptr.state.borrow().is_maximized() { - unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; - } else { - unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; - } - true - } - (HTCLOSE, HTCLOSE) => { - unsafe { - PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default()) - .log_err() - }; - true - } - _ => false, + } else { + drop(lock); }; - if handled { - return Some(0); - } - } - None -} - -fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc) -> Option { - let mut state = state_ptr.state.borrow_mut(); - let had_cursor = state.current_cursor.is_some(); - - state.current_cursor = if lparam.0 == 0 { - None - } else { - Some(HCURSOR(lparam.0 as _)) - }; - - if had_cursor != state.current_cursor.is_some() { - unsafe { SetCursor(state.current_cursor) }; - } - - Some(0) -} - -fn handle_set_cursor( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - if unsafe { !IsWindowEnabled(handle).as_bool() } - || matches!( - lparam.loword() as u32, - HTLEFT - | HTRIGHT - | HTTOP - | HTTOPLEFT - | HTTOPRIGHT - | HTBOTTOM - | HTBOTTOMLEFT - | HTBOTTOMRIGHT - ) - { - return None; - } - unsafe { - SetCursor(state_ptr.state.borrow().current_cursor); - }; - Some(1) -} - -fn handle_system_settings_changed( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - if wparam.0 != 0 { - let mut lock = state_ptr.state.borrow_mut(); - let display = lock.display; - lock.system_settings.update(display, wparam.0); - lock.click_state.system_update(wparam.0); - lock.border_offset.update(handle).log_err(); - } else { - handle_system_theme_changed(handle, lparam, state_ptr)?; - }; - // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide - // taskbar correctly. - notify_frame_changed(handle); - - Some(0) -} - -fn handle_system_command(wparam: WPARAM, state_ptr: Rc) -> Option { - if wparam.0 == SC_KEYMENU as usize { - let mut lock = state_ptr.state.borrow_mut(); - if lock.system_key_handled { - lock.system_key_handled = false; - return Some(0); - } - } - None -} - -fn handle_system_theme_changed( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - // lParam is a pointer to a string that indicates the area containing the system parameter - // that was changed. - let parameter = PCWSTR::from_raw(lparam.0 as _); - if unsafe { !parameter.is_null() && !parameter.is_empty() } { - if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() { - log::info!("System settings changed: {}", parameter_string); - match parameter_string.as_str() { - "ImmersiveColorSet" => { - let new_appearance = system_appearance() - .context("unable to get system appearance when handling ImmersiveColorSet") - .log_err()?; - let mut lock = state_ptr.state.borrow_mut(); - if new_appearance != lock.appearance { - lock.appearance = new_appearance; - let mut callback = lock.callbacks.appearance_changed.take()?; - drop(lock); - callback(); - state_ptr.state.borrow_mut().callbacks.appearance_changed = Some(callback); - configure_dwm_dark_mode(handle, new_appearance); - } - } - _ => {} - } - } - } - Some(0) -} - -fn handle_input_language_changed( - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let thread = state_ptr.main_thread_id_win32; - let validation = state_ptr.validation_number; - unsafe { - PostThreadMessageW(thread, WM_INPUTLANGCHANGE, WPARAM(validation), lparam).log_err(); - } - Some(0) -} - -fn handle_device_change_msg( - handle: HWND, - wparam: WPARAM, - state_ptr: Rc, -) -> Option { - if wparam.0 == DBT_DEVNODES_CHANGED as usize { - // The reason for sending this message is to actually trigger a redraw of the window. - unsafe { - PostMessageW( - Some(handle), - WM_GPUI_FORCE_UPDATE_WINDOW, - WPARAM(0), - LPARAM(0), - ) - .log_err(); - } - // If the GPU device is lost, this redraw will take care of recreating the device context. - // The WM_GPUI_FORCE_UPDATE_WINDOW message will take care of redrawing the window, after - // the device context has been recreated. - draw_window(handle, true, state_ptr) - } else { - // Other device change messages are not handled. - None - } -} - -#[inline] -fn draw_window( - handle: HWND, - force_render: bool, - state_ptr: Rc, -) -> Option { - let mut request_frame = state_ptr - .state - .borrow_mut() - .callbacks - .request_frame - .take()?; - request_frame(RequestFrameOptions { - require_presentation: false, - force_render, - }); - state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame); - unsafe { ValidateRect(Some(handle), None).ok().log_err() }; - Some(0) -} - -#[inline] -fn parse_char_message(wparam: WPARAM, state_ptr: &Rc) -> Option { - let code_point = wparam.loword(); - let mut lock = state_ptr.state.borrow_mut(); - // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630 - match code_point { - 0xD800..=0xDBFF => { - // High surrogate, wait for low surrogate - lock.pending_surrogate = Some(code_point); + // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc + if button == MouseButton::Left { + match wparam.0 as u32 { + HTMINBUTTON => self.state.borrow_mut().nc_button_pressed = Some(HTMINBUTTON), + HTMAXBUTTON => self.state.borrow_mut().nc_button_pressed = Some(HTMAXBUTTON), + HTCLOSE => self.state.borrow_mut().nc_button_pressed = Some(HTCLOSE), + _ => return None, + }; + Some(0) + } else { None } - 0xDC00..=0xDFFF => { - if let Some(high_surrogate) = lock.pending_surrogate.take() { - // Low surrogate, combine with pending high surrogate - String::from_utf16(&[high_surrogate, code_point]).ok() - } else { - // Invalid low surrogate without a preceding high surrogate - log::warn!( - "Received low surrogate without a preceding high surrogate: {code_point:x}" - ); - None + } + + fn handle_nc_mouse_up_msg( + &self, + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let mut lock = self.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + drop(lock); + + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + modifiers: current_modifiers(), + click_count: 1, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { + return Some(0); + } + } else { + drop(lock); + } + + let last_pressed = self.state.borrow_mut().nc_button_pressed.take(); + if button == MouseButton::Left + && let Some(last_pressed) = last_pressed + { + let handled = match (wparam.0 as u32, last_pressed) { + (HTMINBUTTON, HTMINBUTTON) => { + unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; + true + } + (HTMAXBUTTON, HTMAXBUTTON) => { + if self.state.borrow().is_maximized() { + unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; + } else { + unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; + } + true + } + (HTCLOSE, HTCLOSE) => { + unsafe { + PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default()) + .log_err() + }; + true + } + _ => false, + }; + if handled { + return Some(0); } } - _ => { - lock.pending_surrogate = None; - char::from_u32(code_point as u32) - .filter(|c| !c.is_control()) - .map(|c| c.to_string()) + + None + } + + fn handle_cursor_changed(&self, lparam: LPARAM) -> Option { + let mut state = self.state.borrow_mut(); + let had_cursor = state.current_cursor.is_some(); + + state.current_cursor = if lparam.0 == 0 { + None + } else { + Some(HCURSOR(lparam.0 as _)) + }; + + if had_cursor != state.current_cursor.is_some() { + unsafe { SetCursor(state.current_cursor) }; } + + Some(0) + } + + fn handle_set_cursor(&self, handle: HWND, lparam: LPARAM) -> Option { + if unsafe { !IsWindowEnabled(handle).as_bool() } + || matches!( + lparam.loword() as u32, + HTLEFT + | HTRIGHT + | HTTOP + | HTTOPLEFT + | HTTOPRIGHT + | HTBOTTOM + | HTBOTTOMLEFT + | HTBOTTOMRIGHT + ) + { + return None; + } + unsafe { + SetCursor(self.state.borrow().current_cursor); + }; + Some(1) + } + + fn handle_system_settings_changed( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + if wparam.0 != 0 { + let mut lock = self.state.borrow_mut(); + let display = lock.display; + lock.system_settings.update(display, wparam.0); + lock.click_state.system_update(wparam.0); + lock.border_offset.update(handle).log_err(); + } else { + self.handle_system_theme_changed(handle, lparam)?; + }; + // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide + // taskbar correctly. + notify_frame_changed(handle); + + Some(0) + } + + fn handle_system_command(&self, wparam: WPARAM) -> Option { + if wparam.0 == SC_KEYMENU as usize { + let mut lock = self.state.borrow_mut(); + if lock.system_key_handled { + lock.system_key_handled = false; + return Some(0); + } + } + None + } + + fn handle_system_theme_changed(&self, handle: HWND, lparam: LPARAM) -> Option { + // lParam is a pointer to a string that indicates the area containing the system parameter + // that was changed. + let parameter = PCWSTR::from_raw(lparam.0 as _); + if unsafe { !parameter.is_null() && !parameter.is_empty() } { + if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() { + log::info!("System settings changed: {}", parameter_string); + match parameter_string.as_str() { + "ImmersiveColorSet" => { + let new_appearance = system_appearance() + .context( + "unable to get system appearance when handling ImmersiveColorSet", + ) + .log_err()?; + let mut lock = self.state.borrow_mut(); + if new_appearance != lock.appearance { + lock.appearance = new_appearance; + let mut callback = lock.callbacks.appearance_changed.take()?; + drop(lock); + callback(); + self.state.borrow_mut().callbacks.appearance_changed = Some(callback); + configure_dwm_dark_mode(handle, new_appearance); + } + } + _ => {} + } + } + } + Some(0) + } + + fn handle_input_language_changed(&self, lparam: LPARAM) -> Option { + let thread = self.main_thread_id_win32; + let validation = self.validation_number; + unsafe { + PostThreadMessageW(thread, WM_INPUTLANGCHANGE, WPARAM(validation), lparam).log_err(); + } + Some(0) + } + + fn handle_device_change_msg(&self, handle: HWND, wparam: WPARAM) -> Option { + if wparam.0 == DBT_DEVNODES_CHANGED as usize { + // The reason for sending this message is to actually trigger a redraw of the window. + unsafe { + PostMessageW( + Some(handle), + WM_GPUI_FORCE_UPDATE_WINDOW, + WPARAM(0), + LPARAM(0), + ) + .log_err(); + } + // If the GPU device is lost, this redraw will take care of recreating the device context. + // The WM_GPUI_FORCE_UPDATE_WINDOW message will take care of redrawing the window, after + // the device context has been recreated. + self.draw_window(handle, true) + } else { + // Other device change messages are not handled. + None + } + } + + #[inline] + fn draw_window(&self, handle: HWND, force_render: bool) -> Option { + let mut request_frame = self.state.borrow_mut().callbacks.request_frame.take()?; + request_frame(RequestFrameOptions { + require_presentation: false, + force_render, + }); + self.state.borrow_mut().callbacks.request_frame = Some(request_frame); + unsafe { ValidateRect(Some(handle), None).ok().log_err() }; + Some(0) + } + + #[inline] + fn parse_char_message(&self, wparam: WPARAM) -> Option { + let code_point = wparam.loword(); + let mut lock = self.state.borrow_mut(); + // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630 + match code_point { + 0xD800..=0xDBFF => { + // High surrogate, wait for low surrogate + lock.pending_surrogate = Some(code_point); + None + } + 0xDC00..=0xDFFF => { + if let Some(high_surrogate) = lock.pending_surrogate.take() { + // Low surrogate, combine with pending high surrogate + String::from_utf16(&[high_surrogate, code_point]).ok() + } else { + // Invalid low surrogate without a preceding high surrogate + log::warn!( + "Received low surrogate without a preceding high surrogate: {code_point:x}" + ); + None + } + } + _ => { + lock.pending_surrogate = None; + char::from_u32(code_point as u32) + .filter(|c| !c.is_control()) + .map(|c| c.to_string()) + } + } + } + + fn start_tracking_mouse(&self, handle: HWND, flags: TRACKMOUSEEVENT_FLAGS) { + let mut lock = self.state.borrow_mut(); + if !lock.hovered { + lock.hovered = true; + unsafe { + TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: std::mem::size_of::() as u32, + dwFlags: flags, + hwndTrack: handle, + dwHoverTime: HOVER_DEFAULT, + }) + .log_err() + }; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(true); + self.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + } + } + + fn with_input_handler(&self, f: F) -> Option + where + F: FnOnce(&mut PlatformInputHandler) -> R, + { + let mut input_handler = self.state.borrow_mut().input_handler.take()?; + let result = f(&mut input_handler); + self.state.borrow_mut().input_handler = Some(input_handler); + Some(result) + } + + fn with_input_handler_and_scale_factor(&self, f: F) -> Option + where + F: FnOnce(&mut PlatformInputHandler, f32) -> Option, + { + let mut lock = self.state.borrow_mut(); + let mut input_handler = lock.input_handler.take()?; + let scale_factor = lock.scale_factor; + drop(lock); + let result = f(&mut input_handler, scale_factor); + self.state.borrow_mut().input_handler = Some(input_handler); + result } } @@ -1543,54 +1511,3 @@ fn notify_frame_changed(handle: HWND) { .log_err(); } } - -fn start_tracking_mouse( - handle: HWND, - state_ptr: &Rc, - flags: TRACKMOUSEEVENT_FLAGS, -) { - let mut lock = state_ptr.state.borrow_mut(); - if !lock.hovered { - lock.hovered = true; - unsafe { - TrackMouseEvent(&mut TRACKMOUSEEVENT { - cbSize: std::mem::size_of::() as u32, - dwFlags: flags, - hwndTrack: handle, - dwHoverTime: HOVER_DEFAULT, - }) - .log_err() - }; - if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { - drop(lock); - callback(true); - state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback); - } - } -} - -fn with_input_handler(state_ptr: &Rc, f: F) -> Option -where - F: FnOnce(&mut PlatformInputHandler) -> R, -{ - let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?; - let result = f(&mut input_handler); - state_ptr.state.borrow_mut().input_handler = Some(input_handler); - Some(result) -} - -fn with_input_handler_and_scale_factor( - state_ptr: &Rc, - f: F, -) -> Option -where - F: FnOnce(&mut PlatformInputHandler, f32) -> Option, -{ - let mut lock = state_ptr.state.borrow_mut(); - let mut input_handler = lock.input_handler.take()?; - let scale_factor = lock.scale_factor; - drop(lock); - let result = f(&mut input_handler, scale_factor); - state_ptr.state.borrow_mut().input_handler = Some(input_handler); - result -} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index bc09cc199d..01b043a755 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -144,12 +144,12 @@ impl WindowsPlatform { } } - pub fn try_get_windows_inner_from_hwnd(&self, hwnd: HWND) -> Option> { + pub fn window_from_hwnd(&self, hwnd: HWND) -> Option> { self.raw_window_handles .read() .iter() .find(|entry| *entry == &hwnd) - .and_then(|hwnd| try_get_window_inner(*hwnd)) + .and_then(|hwnd| window_from_hwnd(*hwnd)) } #[inline] @@ -434,7 +434,7 @@ impl Platform for WindowsPlatform { fn active_window(&self) -> Option { let active_window_hwnd = unsafe { GetActiveWindow() }; - self.try_get_windows_inner_from_hwnd(active_window_hwnd) + self.window_from_hwnd(active_window_hwnd) .map(|inner| inner.handle) } diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 68b667569b..4043001a35 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -28,7 +28,7 @@ use windows::{ use crate::*; -pub(crate) struct WindowsWindow(pub Rc); +pub(crate) struct WindowsWindow(pub Rc); pub struct WindowsWindowState { pub origin: Point, @@ -61,9 +61,9 @@ pub struct WindowsWindowState { hwnd: HWND, } -pub(crate) struct WindowsWindowStatePtr { +pub(crate) struct WindowsWindowInner { hwnd: HWND, - this: Weak, + pub(super) this: Weak, drop_target_helper: IDropTargetHelper, pub(crate) state: RefCell, pub(crate) handle: AnyWindowHandle, @@ -79,7 +79,7 @@ pub(crate) struct WindowsWindowStatePtr { impl WindowsWindowState { fn new( hwnd: HWND, - cs: &CREATESTRUCTW, + window_params: &CREATESTRUCTW, current_cursor: Option, display: WindowsDisplay, min_size: Option>, @@ -90,9 +90,12 @@ impl WindowsWindowState { let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32; monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32 }; - let origin = logical_point(cs.x as f32, cs.y as f32, scale_factor); + let origin = logical_point(window_params.x as f32, window_params.y as f32, scale_factor); let logical_size = { - let physical_size = size(DevicePixels(cs.cx), DevicePixels(cs.cy)); + let physical_size = size( + DevicePixels(window_params.cx), + DevicePixels(window_params.cy), + ); physical_size.to_pixels(scale_factor) }; let fullscreen_restore_bounds = Bounds { @@ -201,7 +204,7 @@ impl WindowsWindowState { } } -impl WindowsWindowStatePtr { +impl WindowsWindowInner { fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result> { let state = RefCell::new(WindowsWindowState::new( hwnd, @@ -230,13 +233,13 @@ impl WindowsWindowStatePtr { } fn toggle_fullscreen(&self) { - let Some(state_ptr) = self.this.upgrade() else { + let Some(this) = self.this.upgrade() else { log::error!("Unable to toggle fullscreen: window has been dropped"); return; }; self.executor .spawn(async move { - let mut lock = state_ptr.state.borrow_mut(); + let mut lock = this.state.borrow_mut(); let StyleAndBounds { style, x, @@ -248,10 +251,9 @@ impl WindowsWindowStatePtr { } else { let (window_bounds, _) = lock.calculate_window_bounds(); lock.fullscreen_restore_bounds = window_bounds; - let style = - WINDOW_STYLE(unsafe { get_window_long(state_ptr.hwnd, GWL_STYLE) } as _); + let style = WINDOW_STYLE(unsafe { get_window_long(this.hwnd, GWL_STYLE) } as _); let mut rc = RECT::default(); - unsafe { GetWindowRect(state_ptr.hwnd, &mut rc) }.log_err(); + unsafe { GetWindowRect(this.hwnd, &mut rc) }.log_err(); let _ = lock.fullscreen.insert(StyleAndBounds { style, x: rc.left, @@ -275,10 +277,10 @@ impl WindowsWindowStatePtr { } }; drop(lock); - unsafe { set_window_long(state_ptr.hwnd, GWL_STYLE, style.0 as isize) }; + unsafe { set_window_long(this.hwnd, GWL_STYLE, style.0 as isize) }; unsafe { SetWindowPos( - state_ptr.hwnd, + this.hwnd, None, x, y, @@ -328,7 +330,7 @@ pub(crate) struct Callbacks { } struct WindowCreateContext { - inner: Option>>, + inner: Option>>, handle: AnyWindowHandle, hide_title_bar: bool, display: WindowsDisplay, @@ -362,13 +364,13 @@ impl WindowsWindow { main_thread_id_win32, disable_direct_composition, } = creation_info; - let classname = register_wnd_class(icon); + register_window_class(icon); let hide_title_bar = params .titlebar .as_ref() .map(|titlebar| titlebar.appears_transparent) .unwrap_or(true); - let windowname = HSTRING::from( + let window_name = HSTRING::from( params .titlebar .as_ref() @@ -414,12 +416,11 @@ impl WindowsWindow { appearance, disable_direct_composition, }; - let lpparam = Some(&context as *const _ as *const _); let creation_result = unsafe { CreateWindowExW( dwexstyle, - classname, - &windowname, + WINDOW_CLASS_NAME, + &window_name, dwstyle, CW_USEDEFAULT, CW_USEDEFAULT, @@ -428,33 +429,35 @@ impl WindowsWindow { None, None, Some(hinstance.into()), - lpparam, + Some(&context as *const _ as *const _), ) }; - // We should call `?` on state_ptr first, then call `?` on hwnd. - // Or, we will lose the error info reported by `WindowsWindowState::new` - let state_ptr = context.inner.take().unwrap()?; + + // Failure to create a `WindowsWindowState` can cause window creation to fail, + // so check the inner result first. + let this = context.inner.take().unwrap()?; let hwnd = creation_result?; - register_drag_drop(state_ptr.clone())?; + + register_drag_drop(&this)?; configure_dwm_dark_mode(hwnd, appearance); - state_ptr.state.borrow_mut().border_offset.update(hwnd)?; + this.state.borrow_mut().border_offset.update(hwnd)?; let placement = retrieve_window_placement( hwnd, display, params.bounds, - state_ptr.state.borrow().scale_factor, - state_ptr.state.borrow().border_offset, + this.state.borrow().scale_factor, + this.state.borrow().border_offset, )?; if params.show { unsafe { SetWindowPlacement(hwnd, &placement)? }; } else { - state_ptr.state.borrow_mut().initial_placement = Some(WindowOpenStatus { + this.state.borrow_mut().initial_placement = Some(WindowOpenStatus { placement, state: WindowOpenState::Windowed, }); } - Ok(Self(state_ptr)) + Ok(Self(this)) } } @@ -803,7 +806,7 @@ impl PlatformWindow for WindowsWindow { } #[implement(IDropTarget)] -struct WindowsDragDropHandler(pub Rc); +struct WindowsDragDropHandler(pub Rc); impl WindowsDragDropHandler { fn handle_drag_drop(&self, input: PlatformInput) { @@ -1084,15 +1087,15 @@ enum WindowOpenState { Windowed, } -fn register_wnd_class(icon_handle: HICON) -> PCWSTR { - const CLASS_NAME: PCWSTR = w!("Zed::Window"); +const WINDOW_CLASS_NAME: PCWSTR = w!("Zed::Window"); +fn register_window_class(icon_handle: HICON) { static ONCE: Once = Once::new(); ONCE.call_once(|| { let wc = WNDCLASSW { - lpfnWndProc: Some(wnd_proc), + lpfnWndProc: Some(window_procedure), hIcon: icon_handle, - lpszClassName: PCWSTR(CLASS_NAME.as_ptr()), + lpszClassName: PCWSTR(WINDOW_CLASS_NAME.as_ptr()), style: CS_HREDRAW | CS_VREDRAW, hInstance: get_module_handle().into(), hbrBackground: unsafe { CreateSolidBrush(COLORREF(0x00000000)) }, @@ -1100,54 +1103,58 @@ fn register_wnd_class(icon_handle: HICON) -> PCWSTR { }; unsafe { RegisterClassW(&wc) }; }); - - CLASS_NAME } -unsafe extern "system" fn wnd_proc( +unsafe extern "system" fn window_procedure( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { if msg == WM_NCCREATE { - let cs = lparam.0 as *const CREATESTRUCTW; - let cs = unsafe { &*cs }; - let ctx = cs.lpCreateParams as *mut WindowCreateContext; - let ctx = unsafe { &mut *ctx }; - let creation_result = WindowsWindowStatePtr::new(ctx, hwnd, cs); - if creation_result.is_err() { - ctx.inner = Some(creation_result); - return LRESULT(0); - } - let weak = Box::new(Rc::downgrade(creation_result.as_ref().unwrap())); - unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; - ctx.inner = Some(creation_result); - return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; + let window_params = lparam.0 as *const CREATESTRUCTW; + let window_params = unsafe { &*window_params }; + let window_creation_context = window_params.lpCreateParams as *mut WindowCreateContext; + let window_creation_context = unsafe { &mut *window_creation_context }; + return match WindowsWindowInner::new(window_creation_context, hwnd, window_params) { + Ok(window_state) => { + let weak = Box::new(Rc::downgrade(&window_state)); + unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; + window_creation_context.inner = Some(Ok(window_state)); + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + } + Err(error) => { + window_creation_context.inner = Some(Err(error)); + LRESULT(0) + } + }; } - let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; if ptr.is_null() { return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; } let inner = unsafe { &*ptr }; - let r = if let Some(state) = inner.upgrade() { - handle_msg(hwnd, msg, wparam, lparam, state) + let result = if let Some(inner) = inner.upgrade() { + inner.handle_msg(hwnd, msg, wparam, lparam) } else { unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } }; + if msg == WM_NCDESTROY { unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; unsafe { drop(Box::from_raw(ptr)) }; } - r + + result } -pub(crate) fn try_get_window_inner(hwnd: HWND) -> Option> { +pub(crate) fn window_from_hwnd(hwnd: HWND) -> Option> { if hwnd.is_invalid() { return None; } - let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; if !ptr.is_null() { let inner = unsafe { &*ptr }; inner.upgrade() @@ -1170,9 +1177,9 @@ fn get_module_handle() -> HMODULE { } } -fn register_drag_drop(state_ptr: Rc) -> Result<()> { - let window_handle = state_ptr.hwnd; - let handler = WindowsDragDropHandler(state_ptr); +fn register_drag_drop(window: &Rc) -> Result<()> { + let window_handle = window.hwnd; + let handler = WindowsDragDropHandler(window.clone()); // The lifetime of `IDropTarget` is handled by Windows, it won't release until // we call `RevokeDragDrop`. // So, it's safe to drop it here. From 1325bf1420763004238877217df76b13aca760f0 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 4 Aug 2025 15:45:17 -0300 Subject: [PATCH 18/46] Update to acp 0.0.18 (#35595) Release Notes: - N/A --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 12 ++++++------ crates/agent_servers/src/acp/v0.rs | 4 ++-- crates/agent_servers/src/acp/v1.rs | 18 ++++++++++++------ crates/agent_servers/src/claude/mcp_server.rs | 4 ++-- crates/agent_servers/src/claude/tools.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 8 ++++---- 8 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c21aec93ed..cf35a467a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.17" +version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22c5180e40d31a9998ffa5f8eb067667f0870908a4aeed65a6a299e2d1d95443" +checksum = "f8e4c1dccb35e69d32566f0d11948d902f9942fc3f038821816c1150cf5925f4" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 80796018eb..d5982116f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -421,7 +421,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.17" +agent-client-protocol = "0.0.18" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 079a207358..44190a4860 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -178,7 +178,7 @@ impl ToolCall { id: tool_call.id, label: cx.new(|cx| { Markdown::new( - tool_call.label.into(), + tool_call.title.into(), Some(language_registry.clone()), None, cx, @@ -205,7 +205,7 @@ impl ToolCall { let acp::ToolCallUpdateFields { kind, status, - label, + title, content, locations, raw_input, @@ -219,8 +219,8 @@ impl ToolCall { self.status = ToolCallStatus::Allowed { status }; } - if let Some(label) = label { - self.label = cx.new(|cx| Markdown::new_text(label.into(), cx)); + if let Some(title) = title { + self.label = cx.new(|cx| Markdown::new_text(title.into(), cx)); } if let Some(content) = content { @@ -1504,7 +1504,7 @@ mod tests { thread.handle_session_update( acp::SessionUpdate::ToolCall(acp::ToolCall { id: id.clone(), - label: "Label".into(), + title: "Label".into(), kind: acp::ToolKind::Fetch, status: acp::ToolCallStatus::InProgress, content: vec![], @@ -1608,7 +1608,7 @@ mod tests { thread.handle_session_update( acp::SessionUpdate::ToolCall(acp::ToolCall { id: acp::ToolCallId("test".into()), - label: "Label".into(), + title: "Label".into(), kind: acp::ToolKind::Edit, status: acp::ToolCallStatus::Completed, content: vec![acp::ToolCallContent::Diff { diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 6839ff2462..3dcda4ce8d 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -127,7 +127,7 @@ impl acp_old::Client for OldAcpClientDelegate { outcomes.push(outcome); acp_options.push(acp::PermissionOption { id: acp::PermissionOptionId(index.to_string().into()), - label, + name: label, kind, }) } @@ -266,7 +266,7 @@ impl acp_old::Client for OldAcpClientDelegate { fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall { acp::ToolCall { id: id, - label: request.label, + title: request.label, kind: acp_kind_from_old_icon(request.icon), status: acp::ToolCallStatus::InProgress, content: request diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 9e2193ce18..a4f0e996b5 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,4 +1,5 @@ use agent_client_protocol::{self as acp, Agent as _}; +use anyhow::anyhow; use collections::HashMap; use futures::channel::oneshot; use project::Project; @@ -105,11 +106,16 @@ impl AgentConnection for AcpConnection { mcp_servers: vec![], cwd, }) - .await?; + .await + .map_err(|err| { + if err.code == acp::ErrorCode::AUTH_REQUIRED.code { + anyhow!(AuthRequired) + } else { + anyhow!(err) + } + })?; - let Some(session_id) = response.session_id else { - anyhow::bail!(AuthRequired); - }; + let session_id = response.session_id; let thread = cx.new(|cx| { AcpThread::new( @@ -155,11 +161,11 @@ impl AgentConnection for AcpConnection { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let conn = self.connection.clone(); - let params = acp::CancelledNotification { + let params = acp::CancelNotification { session_id: session_id.clone(), }; cx.foreground_executor() - .spawn(async move { conn.cancelled(params).await }) + .spawn(async move { conn.cancel(params).await }) .detach(); } } diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index cc303016f1..c6f8bb5b69 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -158,12 +158,12 @@ impl McpServerTool for PermissionTool { vec![ acp::PermissionOption { id: allow_option_id.clone(), - label: "Allow".into(), + name: "Allow".into(), kind: acp::PermissionOptionKind::AllowOnce, }, acp::PermissionOption { id: reject_option_id.clone(), - label: "Reject".into(), + name: "Reject".into(), kind: acp::PermissionOptionKind::RejectOnce, }, ], diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 6acb6355aa..e7d33e5298 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -308,7 +308,7 @@ impl ClaudeTool { id, kind: self.kind(), status: acp::ToolCallStatus::InProgress, - label: self.label(), + title: self.label(), content: self.content(), locations: self.locations(), raw_input: None, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9ea9209189..a8e2d59b62 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1233,7 +1233,7 @@ impl AcpThreadView { }) .children(options.iter().map(|option| { let option_id = SharedString::from(option.id.0.clone()); - Button::new((option_id, entry_ix), option.label.clone()) + Button::new((option_id, entry_ix), option.name.clone()) .map(|this| match option.kind { acp::PermissionOptionKind::AllowOnce => { this.icon(IconName::Check).icon_color(Color::Success) @@ -2465,7 +2465,7 @@ impl Render for AcpThreadView { connection.auth_methods().into_iter().map(|method| { Button::new( SharedString::from(method.id.0.clone()), - method.label.clone(), + method.name.clone(), ) .on_click({ let method_id = method.id.clone(); @@ -2773,7 +2773,7 @@ mod tests { let tool_call_id = acp::ToolCallId("1".into()); let tool_call = acp::ToolCall { id: tool_call_id.clone(), - label: "Label".into(), + title: "Label".into(), kind: acp::ToolKind::Edit, status: acp::ToolCallStatus::Pending, content: vec!["hi".into()], @@ -2785,7 +2785,7 @@ mod tests { tool_call_id, vec![acp::PermissionOption { id: acp::PermissionOptionId("1".into()), - label: "Allow".into(), + name: "Allow".into(), kind: acp::PermissionOptionKind::AllowOnce, }], )])); From 6e77c6a5ef1e0d6e4b137b4ec07dff1c332c8b2a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:02:29 -0300 Subject: [PATCH 19/46] onboarding: Adjust the welcome page a bit (#35600) Release Notes: - N/A --- crates/onboarding/src/welcome.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 3d2c034367..213032f1b3 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -87,18 +87,18 @@ impl Section { ) -> impl IntoElement { v_flex() .min_w_full() - .gap_2() .child( h_flex() .px_1() - .gap_4() + .mb_2() + .gap_2() .child( Label::new(self.title.to_ascii_uppercase()) .buffer_font(cx) .color(Color::Muted) .size(LabelSize::XSmall), ) - .child(Divider::horizontal().color(DividerColor::Border)), + .child(Divider::horizontal().color(DividerColor::BorderVariant)), ) .children( self.entries @@ -125,10 +125,10 @@ impl SectionEntry { ) -> impl IntoElement { ButtonLike::new(("onboarding-button-id", button_index)) .full_width() + .size(ButtonSize::Medium) .child( h_flex() .w_full() - .gap_1() .justify_between() .child( h_flex() @@ -140,7 +140,10 @@ impl SectionEntry { ) .child(Label::new(self.title)), ) - .children(KeyBinding::for_action_in(self.action, focus, window, cx)), + .children( + KeyBinding::for_action_in(self.action, focus, window, cx) + .map(|s| s.size(rems_from_px(12.))), + ), ) .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx)) } @@ -191,8 +194,8 @@ impl Render for WelcomePage { ) .child( v_flex() - .mt_12() - .gap_8() + .mt_10() + .gap_6() .child(first_section.render( Default::default(), &self.focus_handle, @@ -213,10 +216,9 @@ impl Render for WelcomePage { // We call this a hack .rounded_b_xs() .border_t_1() - .border_color(DividerColor::Border.hsla(cx)) + .border_color(cx.theme().colors().border.opacity(0.6)) .border_dashed() .child( - div().child( Button::new("welcome-exit", "Return to Setup") .full_width() .label_size(LabelSize::XSmall) @@ -278,7 +280,6 @@ impl Render for WelcomePage { }); }), ), - ), ), ), ), From 3df5394a8c4a4f4aebdbe0fac90b862327b7c41a Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 4 Aug 2025 15:35:19 -0400 Subject: [PATCH 20/46] linux: Make desktop file executable (#35597) Closes https://github.com/zed-industries/zed/issues/35545 Release Notes: - linux: Improved support for `zed://` urls on Linux --- crates/zed/resources/flatpak/manifest-template.json | 2 +- docs/src/development/linux.md | 2 +- nix/build.nix | 1 + script/bundle-freebsd | 1 + script/bundle-linux | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/zed/resources/flatpak/manifest-template.json b/crates/zed/resources/flatpak/manifest-template.json index 1560027e9f..0a14a1c2b0 100644 --- a/crates/zed/resources/flatpak/manifest-template.json +++ b/crates/zed/resources/flatpak/manifest-template.json @@ -38,7 +38,7 @@ }, "build-commands": [ "install -Dm644 $ICON_FILE.png /app/share/icons/hicolor/512x512/apps/$APP_ID.png", - "envsubst < zed.desktop.in > zed.desktop && install -Dm644 zed.desktop /app/share/applications/$APP_ID.desktop", + "envsubst < zed.desktop.in > zed.desktop && install -Dm755 zed.desktop /app/share/applications/$APP_ID.desktop", "envsubst < flatpak/zed.metainfo.xml.in > zed.metainfo.xml && install -Dm644 zed.metainfo.xml /app/share/metainfo/$APP_ID.metainfo.xml", "sed -i -e '/@release_info@/{r flatpak/release-info/$CHANNEL' -e 'd}' /app/share/metainfo/$APP_ID.metainfo.xml", "install -Dm755 bin/zed /app/bin/zed", diff --git a/docs/src/development/linux.md b/docs/src/development/linux.md index 6fff25f6c1..d7b586be34 100644 --- a/docs/src/development/linux.md +++ b/docs/src/development/linux.md @@ -91,7 +91,7 @@ Zed has two main binaries: - You will need to build `crates/cli` and make its binary available in `$PATH` with the name `zed`. - You will need to build `crates/zed` and put it at `$PATH/to/cli/../../libexec/zed-editor`. For example, if you are going to put the cli at `~/.local/bin/zed` put zed at `~/.local/libexec/zed-editor`. As some linux distributions (notably Arch) discourage the use of `libexec`, you can also put this binary at `$PATH/to/cli/../../lib/zed/zed-editor` (e.g. `~/.local/lib/zed/zed-editor`) instead. -- If you are going to provide a `.desktop` file you can find a template in `crates/zed/resources/zed.desktop.in`, and use `envsubst` to populate it with the values required. This file should also be renamed to `$APP_ID.desktop` so that the file [follows the FreeDesktop standards](https://github.com/zed-industries/zed/issues/12707#issuecomment-2168742761). +- If you are going to provide a `.desktop` file you can find a template in `crates/zed/resources/zed.desktop.in`, and use `envsubst` to populate it with the values required. This file should also be renamed to `$APP_ID.desktop` so that the file [follows the FreeDesktop standards](https://github.com/zed-industries/zed/issues/12707#issuecomment-2168742761). You should also make this desktop file executable (`chmod 755`). - You will need to ensure that the necessary libraries are installed. You can get the current list by [inspecting the built binary](https://github.com/zed-industries/zed/blob/935cf542aebf55122ce6ed1c91d0fe8711970c82/script/bundle-linux#L65-L67) on your system. - For an example of a complete build script, see [script/bundle-linux](https://github.com/zed-industries/zed/blob/935cf542aebf55122ce6ed1c91d0fe8711970c82/script/bundle-linux). - You can disable Zed's auto updates and provide instructions for users who try to update Zed manually by building (or running) Zed with the environment variable `ZED_UPDATE_EXPLANATION`. For example: `ZED_UPDATE_EXPLANATION="Please use flatpak to update zed."`. diff --git a/nix/build.nix b/nix/build.nix index 873431a427..70b4f76932 100644 --- a/nix/build.nix +++ b/nix/build.nix @@ -298,6 +298,7 @@ craneLib.buildPackage ( export APP_ARGS="%U" mkdir -p "$out/share/applications" ${lib.getExe envsubst} < "crates/zed/resources/zed.desktop.in" > "$out/share/applications/dev.zed.Zed-Nightly.desktop" + chmod +x "$out/share/applications/dev.zed.Zed-Nightly.desktop" ) runHook postInstall diff --git a/script/bundle-freebsd b/script/bundle-freebsd index 7222a06256..87c9459ffb 100755 --- a/script/bundle-freebsd +++ b/script/bundle-freebsd @@ -138,6 +138,7 @@ fi # mkdir -p "${zed_dir}/share/applications" # envsubst <"crates/zed/resources/zed.desktop.in" >"${zed_dir}/share/applications/zed$suffix.desktop" +# chmod +x "${zed_dir}/share/applications/zed$suffix.desktop" # Copy generated licenses so they'll end up in archive too # cp "assets/licenses.md" "${zed_dir}/licenses.md" diff --git a/script/bundle-linux b/script/bundle-linux index 64de62ce9b..ad67b7a0f7 100755 --- a/script/bundle-linux +++ b/script/bundle-linux @@ -179,6 +179,7 @@ fi mkdir -p "${zed_dir}/share/applications" envsubst < "crates/zed/resources/zed.desktop.in" > "${zed_dir}/share/applications/zed$suffix.desktop" +chmod +x "${zed_dir}/share/applications/zed$suffix.desktop" # Copy generated licenses so they'll end up in archive too cp "assets/licenses.md" "${zed_dir}/licenses.md" From 68c24655e98f31991513336a0ead975885d7c294 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 4 Aug 2025 14:18:06 -0600 Subject: [PATCH 21/46] zeta: Collect git sha / remote urls when data collection from OSS is enabled (#35514) Release Notes: - Edit Prediction: Added Git info to edit predictions requests (only sent for opensource projects when data collection is enabled). The sent Git info is the SHA of the current commit and the URLs for the `origin` and `upstream` remotes. --- .../cloud_llm_client/src/cloud_llm_client.rs | 16 +++++++ crates/project/src/git_store.rs | 10 +++++ crates/zeta/src/zeta.rs | 44 ++++++++++++++++++- crates/zeta_cli/src/main.rs | 2 + 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/crates/cloud_llm_client/src/cloud_llm_client.rs b/crates/cloud_llm_client/src/cloud_llm_client.rs index 171c923154..e78957ec49 100644 --- a/crates/cloud_llm_client/src/cloud_llm_client.rs +++ b/crates/cloud_llm_client/src/cloud_llm_client.rs @@ -149,6 +149,22 @@ pub struct PredictEditsBody { pub can_collect_data: bool, #[serde(skip_serializing_if = "Option::is_none", default)] pub diagnostic_groups: Option>, + /// Info about the git repository state, only present when can_collect_data is true. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub git_info: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PredictEditsGitInfo { + /// SHA of git HEAD commit at time of prediction. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub head_sha: Option, + /// URL of the remote called `origin`. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub remote_origin_url: Option, + /// URL of the remote called `upstream`. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub remote_upstream_url: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index c9f0fc7959..01fc987816 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -246,6 +246,8 @@ pub struct RepositorySnapshot { pub head_commit: Option, pub scan_id: u64, pub merge: MergeDetails, + pub remote_origin_url: Option, + pub remote_upstream_url: Option, } type JobId = u64; @@ -2673,6 +2675,8 @@ impl RepositorySnapshot { head_commit: None, scan_id: 0, merge: Default::default(), + remote_origin_url: None, + remote_upstream_url: None, } } @@ -4818,6 +4822,10 @@ async fn compute_snapshot( None => None, }; + // Used by edit prediction data collection + let remote_origin_url = backend.remote_url("origin"); + let remote_upstream_url = backend.remote_url("upstream"); + let snapshot = RepositorySnapshot { id, statuses_by_path, @@ -4826,6 +4834,8 @@ async fn compute_snapshot( branch, head_commit, merge: merge_details, + remote_origin_url, + remote_upstream_url, }; Ok((snapshot, events)) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 1cd8e8d17f..b1bd737dbf 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -19,7 +19,7 @@ use arrayvec::ArrayVec; use client::{Client, EditPredictionUsage, UserStore}; use cloud_llm_client::{ AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, - PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME, + PredictEditsBody, PredictEditsGitInfo, PredictEditsResponse, ZED_VERSION_HEADER_NAME, }; use collections::{HashMap, HashSet, VecDeque}; use futures::AsyncReadExt; @@ -34,7 +34,7 @@ use language::{ }; use language_model::{LlmApiToken, RefreshLlmTokenListener}; use postage::watch; -use project::Project; +use project::{Project, ProjectPath}; use release_channel::AppVersion; use settings::WorktreeId; use std::str::FromStr; @@ -400,6 +400,14 @@ impl Zeta { let llm_token = self.llm_token.clone(); let app_version = AppVersion::global(cx); + let git_info = if let (true, Some(project), Some(file)) = + (can_collect_data, project, snapshot.file()) + { + git_info_for_file(project, &ProjectPath::from_file(file.as_ref(), cx), cx) + } else { + None + }; + let full_path: Arc = snapshot .file() .map(|f| Arc::from(f.full_path(cx).as_path())) @@ -415,6 +423,7 @@ impl Zeta { cursor_point, make_events_prompt, can_collect_data, + git_info, cx, ); @@ -1155,6 +1164,35 @@ fn common_prefix, T2: Iterator>(a: T1, b: .sum() } +fn git_info_for_file( + project: &Entity, + project_path: &ProjectPath, + cx: &App, +) -> Option { + let git_store = project.read(cx).git_store().read(cx); + if let Some((repository, _repo_path)) = + git_store.repository_and_path_for_project_path(project_path, cx) + { + let repository = repository.read(cx); + let head_sha = repository + .head_commit + .as_ref() + .map(|head_commit| head_commit.sha.to_string()); + let remote_origin_url = repository.remote_origin_url.clone(); + let remote_upstream_url = repository.remote_upstream_url.clone(); + if head_sha.is_none() && remote_origin_url.is_none() && remote_upstream_url.is_none() { + return None; + } + Some(PredictEditsGitInfo { + head_sha, + remote_origin_url, + remote_upstream_url, + }) + } else { + None + } +} + pub struct GatherContextOutput { pub body: PredictEditsBody, pub editable_range: Range, @@ -1167,6 +1205,7 @@ pub fn gather_context( cursor_point: language::Point, make_events_prompt: impl FnOnce() -> String + Send + 'static, can_collect_data: bool, + git_info: Option, cx: &App, ) -> Task> { let local_lsp_store = @@ -1216,6 +1255,7 @@ pub fn gather_context( outline: Some(input_outline), can_collect_data, diagnostic_groups, + git_info, }; Ok(GatherContextOutput { diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index c5374b56c9..adf7683152 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -172,6 +172,7 @@ async fn get_context( None => String::new(), }; let can_collect_data = false; + let git_info = None; cx.update(|cx| { gather_context( project.as_ref(), @@ -180,6 +181,7 @@ async fn get_context( clipped_cursor, move || events, can_collect_data, + git_info, cx, ) })? From 24e7f868ad24439e8723005f6fb62d55c01d0e45 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 4 Aug 2025 16:56:56 -0500 Subject: [PATCH 22/46] onboarding: Go back to not having system be separate (#35499) Going back to having system be mutually exclusive with light/dark to simplify the system. We instead just show both light and dark when system is selected Release Notes: - N/A --------- Co-authored-by: Danilo Leal Co-authored-by: Anthony Eid --- crates/gpui/src/geometry.rs | 2 +- crates/onboarding/src/basics_page.rs | 302 +++++++++--------- crates/onboarding/src/theme_preview.rs | 421 ++++++++++++++++--------- 3 files changed, 415 insertions(+), 310 deletions(-) diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 74be6344f9..3d2d9cd9db 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -3522,7 +3522,7 @@ impl Serialize for Length { /// # Returns /// /// A `DefiniteLength` representing the relative length as a fraction of the parent's size. -pub fn relative(fraction: f32) -> DefiniteLength { +pub const fn relative(fraction: f32) -> DefiniteLength { DefiniteLength::Fraction(fraction) } diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 82688e6220..21ea74f01c 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -1,42 +1,24 @@ +use std::sync::Arc; + use client::TelemetrySettings; use fs::Fs; -use gpui::{App, Entity, IntoElement, Window}; +use gpui::{App, IntoElement, Window}; use settings::{BaseKeymap, Settings, update_settings_file}; -use theme::{Appearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, ThemeSettings}; +use theme::{ + Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, + ThemeSettings, +}; use ui::{ ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px, }; use vim_mode_setting::VimModeSetting; -use crate::theme_preview::ThemePreviewTile; +use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; -/// separates theme "mode" ("dark" | "light" | "system") into two separate states -/// - appearance = "dark" | "light" -/// - "system" true/false -/// when system selected: -/// - toggling between light and dark does not change theme.mode, just which variant will be changed -/// when system not selected: -/// - toggling between light and dark does change theme.mode -/// selecting a theme preview will always change theme.["light" | "dark"] to the selected theme, -/// -/// this allows for selecting a dark and light theme option regardless of whether the mode is set to system or not -/// it does not support setting theme to a static value -fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement { let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone(); let system_appearance = theme::SystemAppearance::global(cx); - let appearance_state = window.use_state(cx, |_, _cx| { - theme_selection - .as_ref() - .and_then(|selection| selection.mode()) - .and_then(|mode| match mode { - ThemeMode::System => None, - ThemeMode::Light => Some(Appearance::Light), - ThemeMode::Dark => Some(Appearance::Dark), - }) - .unwrap_or(*system_appearance) - }); - let appearance = *appearance_state.read(cx); let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic { mode: match *system_appearance { Appearance::Light => ThemeMode::Light, @@ -45,70 +27,13 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { light: ThemeName("One Light".into()), dark: ThemeName("One Dark".into()), }); - let theme_registry = ThemeRegistry::global(cx); - let current_theme_name = theme_selection.theme(appearance); - let theme_mode = theme_selection.mode().unwrap_or_default(); - - // let theme_mode = theme_selection.mode(); - // TODO: Clean this up once the "System" button inside the - // toggle button group is done - - let selected_index = match appearance { - Appearance::Light => 0, - Appearance::Dark => 1, - }; - - let theme_seed = 0xBEEF as f32; - - const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; - const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; - - let theme_names = match appearance { - Appearance::Light => LIGHT_THEMES, - Appearance::Dark => DARK_THEMES, - }; - let themes = theme_names - .map(|theme_name| theme_registry.get(theme_name)) - .map(Result::unwrap); - - let theme_previews = themes.map(|theme| { - let is_selected = theme.name == current_theme_name; - let name = theme.name.clone(); - let colors = cx.theme().colors(); - - v_flex() - .id(name.clone()) - .w_full() - .items_center() - .gap_1() - .child( - div() - .w_full() - .border_2() - .border_color(colors.border_transparent) - .rounded(ThemePreviewTile::CORNER_RADIUS) - .map(|this| { - if is_selected { - this.border_color(colors.border_selected) - } else { - this.opacity(0.8).hover(|s| s.border_color(colors.border)) - } - }) - .child(ThemePreviewTile::new(theme.clone(), theme_seed)), - ) - .child(Label::new(name).color(Color::Muted).size(LabelSize::Small)) - .on_click({ - let theme_name = theme.name.clone(); - move |_, _, cx| { - let fs = ::global(cx); - let theme_name = theme_name.clone(); - update_settings_file::(fs, cx, move |settings, _| { - settings.set_theme(theme_name, appearance); - }); - } - }) - }); + let theme_mode = theme_selection + .mode() + .unwrap_or_else(|| match *system_appearance { + Appearance::Light => ThemeMode::Light, + Appearance::Dark => ThemeMode::Dark, + }); return v_flex() .gap_2() @@ -116,93 +41,148 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { h_flex().justify_between().child(Label::new("Theme")).child( ToggleButtonGroup::single_row( "theme-selector-onboarding-dark-light", - [ - ToggleButtonSimple::new("Light", { - let appearance_state = appearance_state.clone(); + [ThemeMode::Light, ThemeMode::Dark, ThemeMode::System].map(|mode| { + const MODE_NAMES: [SharedString; 3] = [ + SharedString::new_static("Light"), + SharedString::new_static("Dark"), + SharedString::new_static("System"), + ]; + ToggleButtonSimple::new( + MODE_NAMES[mode as usize].clone(), move |_, _, cx| { - write_appearance_change(&appearance_state, Appearance::Light, cx); - } - }), - ToggleButtonSimple::new("Dark", { - let appearance_state = appearance_state.clone(); - move |_, _, cx| { - write_appearance_change(&appearance_state, Appearance::Dark, cx); - } - }), - // TODO: Properly put the System back as a button within this group - // Currently, given "System" is not an option in the Appearance enum, - // this button doesn't get selected - ToggleButtonSimple::new("System", { - let theme = theme_selection.clone(); - move |_, _, cx| { - toggle_system_theme_mode(theme.clone(), appearance, cx); - } - }) - .selected(theme_mode == ThemeMode::System), - ], + write_mode_change(mode, cx); + }, + ) + }), ) - .selected_index(selected_index) + .selected_index(theme_mode as usize) .style(ui::ToggleButtonGroupStyle::Outlined) .button_width(rems_from_px(64.)), ), ) - .child(h_flex().gap_4().justify_between().children(theme_previews)); + .child( + h_flex() + .gap_4() + .justify_between() + .children(render_theme_previews(&theme_selection, cx)), + ); - fn write_appearance_change( - appearance_state: &Entity, - new_appearance: Appearance, + fn render_theme_previews( + theme_selection: &ThemeSelection, cx: &mut App, - ) { - let fs = ::global(cx); - appearance_state.write(cx, new_appearance); + ) -> [impl IntoElement; 3] { + let system_appearance = SystemAppearance::global(cx); + let theme_registry = ThemeRegistry::global(cx); - update_settings_file::(fs, cx, move |settings, _| { - if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) { - return; - } - let new_mode = match new_appearance { + let theme_seed = 0xBEEF as f32; + let theme_mode = theme_selection + .mode() + .unwrap_or_else(|| match *system_appearance { Appearance::Light => ThemeMode::Light, Appearance::Dark => ThemeMode::Dark, - }; - settings.set_mode(new_mode); + }); + let appearance = match theme_mode { + ThemeMode::Light => Appearance::Light, + ThemeMode::Dark => Appearance::Dark, + ThemeMode::System => *system_appearance, + }; + let current_theme_name = theme_selection.theme(appearance); + + const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; + const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; + const FAMILY_NAMES: [SharedString; 3] = [ + SharedString::new_static("One"), + SharedString::new_static("Ayu"), + SharedString::new_static("Gruvbox"), + ]; + + let theme_names = match appearance { + Appearance::Light => LIGHT_THEMES, + Appearance::Dark => DARK_THEMES, + }; + + let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap()); + + let theme_previews = [0, 1, 2].map(|index| { + let theme = &themes[index]; + let is_selected = theme.name == current_theme_name; + let name = theme.name.clone(); + let colors = cx.theme().colors(); + + v_flex() + .id(name.clone()) + .w_full() + .items_center() + .gap_1() + .child( + h_flex() + .relative() + .w_full() + .border_2() + .border_color(colors.border_transparent) + .rounded(ThemePreviewTile::ROOT_RADIUS) + .map(|this| { + if is_selected { + this.border_color(colors.border_selected) + } else { + this.opacity(0.8).hover(|s| s.border_color(colors.border)) + } + }) + .map(|this| { + if theme_mode == ThemeMode::System { + let (light, dark) = ( + theme_registry.get(LIGHT_THEMES[index]).unwrap(), + theme_registry.get(DARK_THEMES[index]).unwrap(), + ); + this.child( + ThemePreviewTile::new(light, theme_seed) + .style(ThemePreviewStyle::SideBySide(dark)), + ) + } else { + this.child( + ThemePreviewTile::new(theme.clone(), theme_seed) + .style(ThemePreviewStyle::Bordered), + ) + } + }), + ) + .child( + Label::new(FAMILY_NAMES[index].clone()) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .on_click({ + let theme_name = theme.name.clone(); + move |_, _, cx| { + write_theme_change(theme_name.clone(), theme_mode, cx); + } + }) + }); + + theme_previews + } + + fn write_mode_change(mode: ThemeMode, cx: &mut App) { + let fs = ::global(cx); + update_settings_file::(fs, cx, move |settings, _cx| { + settings.set_mode(mode); }); } - fn toggle_system_theme_mode( - theme_selection: ThemeSelection, - appearance: Appearance, - cx: &mut App, - ) { + fn write_theme_change(theme: impl Into>, theme_mode: ThemeMode, cx: &mut App) { let fs = ::global(cx); - - update_settings_file::(fs, cx, move |settings, _| { - settings.theme = Some(match theme_selection { - ThemeSelection::Static(theme_name) => ThemeSelection::Dynamic { + let theme = theme.into(); + update_settings_file::(fs, cx, move |settings, cx| { + if theme_mode == ThemeMode::System { + settings.theme = Some(ThemeSelection::Dynamic { mode: ThemeMode::System, - light: theme_name.clone(), - dark: theme_name.clone(), - }, - ThemeSelection::Dynamic { - mode: ThemeMode::System, - light, - dark, - } => { - let mode = match appearance { - Appearance::Light => ThemeMode::Light, - Appearance::Dark => ThemeMode::Dark, - }; - ThemeSelection::Dynamic { mode, light, dark } - } - ThemeSelection::Dynamic { - mode: _, - light, - dark, - } => ThemeSelection::Dynamic { - mode: ThemeMode::System, - light, - dark, - }, - }); + light: ThemeName(theme.clone()), + dark: ThemeName(theme.clone()), + }); + } else { + let appearance = *SystemAppearance::global(cx); + settings.set_theme(theme.clone(), appearance); + } }); } } diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index d51511b7f4..53631be1c9 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -1,175 +1,300 @@ #![allow(unused, dead_code)] use gpui::{Hsla, Length}; use std::sync::Arc; -use theme::{Theme, ThemeRegistry}; +use theme::{Theme, ThemeColors, ThemeRegistry}; use ui::{ IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius, }; +#[derive(Clone, PartialEq)] +pub enum ThemePreviewStyle { + Bordered, + Borderless, + SideBySide(Arc), +} + /// Shows a preview of a theme as an abstract illustration /// of a thumbnail-sized editor. #[derive(IntoElement, RegisterComponent, Documented)] pub struct ThemePreviewTile { theme: Arc, seed: f32, + style: ThemePreviewStyle, } impl ThemePreviewTile { - pub const CORNER_RADIUS: Pixels = px(8.0); + pub const SKELETON_HEIGHT_DEFAULT: Pixels = px(2.); + pub const SIDEBAR_SKELETON_ITEM_COUNT: usize = 8; + pub const SIDEBAR_WIDTH_DEFAULT: DefiniteLength = relative(0.25); + pub const ROOT_RADIUS: Pixels = px(8.0); + pub const ROOT_BORDER: Pixels = px(2.0); + pub const ROOT_PADDING: Pixels = px(2.0); + pub const CHILD_BORDER: Pixels = px(1.0); + pub const CHILD_RADIUS: std::cell::LazyCell = std::cell::LazyCell::new(|| { + inner_corner_radius( + Self::ROOT_RADIUS, + Self::ROOT_BORDER, + Self::ROOT_PADDING, + Self::CHILD_BORDER, + ) + }); pub fn new(theme: Arc, seed: f32) -> Self { - Self { theme, seed } + Self { + theme, + seed, + style: ThemePreviewStyle::Bordered, + } + } + + pub fn style(mut self, style: ThemePreviewStyle) -> Self { + self.style = style; + self + } + + pub fn item_skeleton(w: Length, h: Length, bg: Hsla) -> impl IntoElement { + div().w(w).h(h).rounded_full().bg(bg) + } + + pub fn render_sidebar_skeleton_items( + seed: f32, + colors: &ThemeColors, + skeleton_height: impl Into + Clone, + ) -> [impl IntoElement; Self::SIDEBAR_SKELETON_ITEM_COUNT] { + let skeleton_height = skeleton_height.into(); + std::array::from_fn(|index| { + let width = { + let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5; + 0.5 + value * 0.45 + }; + Self::item_skeleton( + relative(width).into(), + skeleton_height, + colors.text.alpha(0.45), + ) + }) + } + + pub fn render_pseudo_code_skeleton( + seed: f32, + theme: Arc, + skeleton_height: impl Into, + ) -> impl IntoElement { + let colors = theme.colors(); + let syntax = theme.syntax(); + + let keyword_color = syntax.get("keyword").color; + let function_color = syntax.get("function").color; + let string_color = syntax.get("string").color; + let comment_color = syntax.get("comment").color; + let variable_color = syntax.get("variable").color; + let type_color = syntax.get("type").color; + let punctuation_color = syntax.get("punctuation").color; + + let syntax_colors = [ + keyword_color, + function_color, + string_color, + variable_color, + type_color, + punctuation_color, + comment_color, + ]; + + let skeleton_height = skeleton_height.into(); + + let line_width = |line_idx: usize, block_idx: usize| -> f32 { + let val = + (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin() * 0.5 + 0.5; + 0.05 + val * 0.2 + }; + + let indentation = |line_idx: usize| -> f32 { + let step = line_idx % 6; + if step < 3 { + step as f32 * 0.1 + } else { + (5 - step) as f32 * 0.1 + } + }; + + let pick_color = |line_idx: usize, block_idx: usize| -> Hsla { + let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin() * 3.5) + .abs() as usize + % syntax_colors.len(); + syntax_colors[idx].unwrap_or(colors.text) + }; + + let line_count = 13; + + let lines = (0..line_count) + .map(|line_idx| { + let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5) * 3.0) + .round() as usize + + 2; + + let indent = indentation(line_idx); + + let blocks = (0..block_count) + .map(|block_idx| { + let width = line_width(line_idx, block_idx); + let color = pick_color(line_idx, block_idx); + Self::item_skeleton(relative(width).into(), skeleton_height, color) + }) + .collect::>(); + + h_flex().gap(px(2.)).ml(relative(indent)).children(blocks) + }) + .collect::>(); + + v_flex().size_full().p_1().gap_1p5().children(lines) + } + + pub fn render_sidebar( + seed: f32, + colors: &ThemeColors, + width: impl Into + Clone, + skeleton_height: impl Into, + ) -> impl IntoElement { + div() + .h_full() + .w(width) + .border_r(px(1.)) + .border_color(colors.border_transparent) + .bg(colors.panel_background) + .child(v_flex().p_2().size_full().gap_1().children( + Self::render_sidebar_skeleton_items(seed, colors, skeleton_height.into()), + )) + } + + pub fn render_pane( + seed: f32, + theme: Arc, + skeleton_height: impl Into, + ) -> impl IntoElement { + v_flex().h_full().flex_grow().child( + div() + .size_full() + .overflow_hidden() + .bg(theme.colors().editor_background) + .p_2() + .child(Self::render_pseudo_code_skeleton( + seed, + theme, + skeleton_height.into(), + )), + ) + } + + pub fn render_editor( + seed: f32, + theme: Arc, + sidebar_width: impl Into + Clone, + skeleton_height: impl Into + Clone, + ) -> impl IntoElement { + div() + .size_full() + .flex() + .bg(theme.colors().background.alpha(1.00)) + .child(Self::render_sidebar( + seed, + theme.colors(), + sidebar_width, + skeleton_height.clone(), + )) + .child(Self::render_pane(seed, theme, skeleton_height.clone())) + } + + fn render_borderless(seed: f32, theme: Arc) -> impl IntoElement { + return Self::render_editor( + seed, + theme, + Self::SIDEBAR_WIDTH_DEFAULT, + Self::SKELETON_HEIGHT_DEFAULT, + ); + } + + fn render_border(seed: f32, theme: Arc) -> impl IntoElement { + div() + .size_full() + .p(Self::ROOT_PADDING) + .rounded(Self::ROOT_RADIUS) + .child( + div() + .size_full() + .rounded(*Self::CHILD_RADIUS) + .border(Self::CHILD_BORDER) + .border_color(theme.colors().border) + .child(Self::render_editor( + seed, + theme.clone(), + Self::SIDEBAR_WIDTH_DEFAULT, + Self::SKELETON_HEIGHT_DEFAULT, + )), + ) + } + + fn render_side_by_side( + seed: f32, + theme: Arc, + other_theme: Arc, + border_color: Hsla, + ) -> impl IntoElement { + let sidebar_width = relative(0.20); + + return div() + .size_full() + .p(Self::ROOT_PADDING) + .rounded(Self::ROOT_RADIUS) + .child( + h_flex() + .size_full() + .relative() + .rounded(*Self::CHILD_RADIUS) + .border(Self::CHILD_BORDER) + .border_color(border_color) + .overflow_hidden() + .child(div().size_full().child(Self::render_editor( + seed, + theme.clone(), + sidebar_width, + Self::SKELETON_HEIGHT_DEFAULT, + ))) + .child( + div() + .size_full() + .absolute() + .left_1_2() + .bg(other_theme.colors().editor_background) + .child(Self::render_editor( + seed, + other_theme, + sidebar_width, + Self::SKELETON_HEIGHT_DEFAULT, + )), + ), + ) + .into_any_element(); } } impl RenderOnce for ThemePreviewTile { fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement { - let color = self.theme.colors(); - - let root_radius = Self::CORNER_RADIUS; - let root_border = px(2.0); - let root_padding = px(2.0); - let child_border = px(1.0); - let inner_radius = - inner_corner_radius(root_radius, root_border, root_padding, child_border); - - let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg); - - let skeleton_height = px(2.); - - let sidebar_seeded_width = |seed: f32, index: usize| { - let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5; - 0.5 + value * 0.45 - }; - - let sidebar_skeleton_items = 8; - - let sidebar_skeleton = (0..sidebar_skeleton_items) - .map(|i| { - let width = sidebar_seeded_width(self.seed, i); - item_skeleton( - relative(width).into(), - skeleton_height, - color.text.alpha(0.45), - ) - }) - .collect::>(); - - let sidebar = div() - .h_full() - .w(relative(0.25)) - .border_r(px(1.)) - .border_color(color.border_transparent) - .bg(color.panel_background) - .child( - v_flex() - .p_2() - .size_full() - .gap_1() - .children(sidebar_skeleton), - ); - - let pseudo_code_skeleton = |theme: Arc, seed: f32| -> AnyElement { - let colors = theme.colors(); - let syntax = theme.syntax(); - - let keyword_color = syntax.get("keyword").color; - let function_color = syntax.get("function").color; - let string_color = syntax.get("string").color; - let comment_color = syntax.get("comment").color; - let variable_color = syntax.get("variable").color; - let type_color = syntax.get("type").color; - let punctuation_color = syntax.get("punctuation").color; - - let syntax_colors = [ - keyword_color, - function_color, - string_color, - variable_color, - type_color, - punctuation_color, - comment_color, - ]; - - let line_width = |line_idx: usize, block_idx: usize| -> f32 { - let val = (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin() - * 0.5 - + 0.5; - 0.05 + val * 0.2 - }; - - let indentation = |line_idx: usize| -> f32 { - let step = line_idx % 6; - if step < 3 { - step as f32 * 0.1 - } else { - (5 - step) as f32 * 0.1 - } - }; - - let pick_color = |line_idx: usize, block_idx: usize| -> Hsla { - let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin() - * 3.5) - .abs() as usize - % syntax_colors.len(); - syntax_colors[idx].unwrap_or(colors.text) - }; - - let line_count = 13; - - let lines = (0..line_count) - .map(|line_idx| { - let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5) - * 3.0) - .round() as usize - + 2; - - let indent = indentation(line_idx); - - let blocks = (0..block_count) - .map(|block_idx| { - let width = line_width(line_idx, block_idx); - let color = pick_color(line_idx, block_idx); - item_skeleton(relative(width).into(), skeleton_height, color) - }) - .collect::>(); - - h_flex().gap(px(2.)).ml(relative(indent)).children(blocks) - }) - .collect::>(); - - v_flex() - .size_full() - .p_1() - .gap_1p5() - .children(lines) - .into_any_element() - }; - - let pane = v_flex().h_full().flex_grow().child( - div() - .size_full() - .overflow_hidden() - .bg(color.editor_background) - .p_2() - .child(pseudo_code_skeleton(self.theme.clone(), self.seed)), - ); - - let content = div().size_full().flex().child(sidebar).child(pane); - - div() - .size_full() - .rounded(root_radius) - .p(root_padding) - .child( - div() - .size_full() - .rounded(inner_radius) - .border(child_border) - .border_color(color.border) - .bg(color.background) - .child(content), + match self.style { + ThemePreviewStyle::Bordered => { + Self::render_border(self.seed, self.theme).into_any_element() + } + ThemePreviewStyle::Borderless => { + Self::render_borderless(self.seed, self.theme).into_any_element() + } + ThemePreviewStyle::SideBySide(other_theme) => Self::render_side_by_side( + self.seed, + self.theme, + other_theme, + _cx.theme().colors().border, ) + .into_any_element(), + } } } From 182edbf526302000ca0e6cf5a14085a6a6b1034c Mon Sep 17 00:00:00 2001 From: Guillaume Launay Date: Tue, 5 Aug 2025 00:20:20 +0200 Subject: [PATCH 23/46] git_panel: Improve toast messages for push/pull/fetch (#35092) On GitLab, when pushing a branch and a MR already existing the remote log contains "View merge request" and the link to the MR. Fixed `Already up to date` stdout check on pull (was `Everything up to date` on stderr) Fixed `Everything up-to-date` check on push (was `Everything up to date`) Improved messaging for up-to-date for fetch/push/pull Fixed tests introduced in https://github.com/zed-industries/zed/pull/33833. Screenshot 2025-07-31 at 18 37 05 Release Notes: - Git UI: Add "View Pull Request" when pushing to Gitlab remotes - git: Improved toast messages on fetch/push/pull --------- Co-authored-by: Peter Tripp --- Cargo.lock | 1 + crates/git_ui/Cargo.toml | 1 + crates/git_ui/src/git_panel.rs | 8 +- crates/git_ui/src/remote_output.rs | 150 +++++++++++++++++------------ 4 files changed, 95 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf35a467a2..69386b3020 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6369,6 +6369,7 @@ dependencies = [ "fuzzy", "git", "gpui", + "indoc", "itertools 0.14.0", "language", "language_model", diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index e6547e7ae9..35f7a60354 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -70,6 +70,7 @@ windows.workspace = true ctor.workspace = true editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +indoc.workspace = true pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 344fa86142..44222b8299 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2899,7 +2899,9 @@ impl GitPanel { let status_toast = StatusToast::new(message, cx, move |this, _cx| { use remote_output::SuccessStyle::*; match style { - Toast { .. } => this, + Toast { .. } => { + this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) + } ToastWithLog { output } => this .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) .action("View Log", move |window, cx| { @@ -2912,9 +2914,9 @@ impl GitPanel { }) .ok(); }), - PushPrLink { link } => this + PushPrLink { text, link } => this .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) - .action("Open Pull Request", move |_, cx| cx.open_url(&link)), + .action(text, move |_, cx| cx.open_url(&link)), } }); workspace.toggle_status_toast(status_toast, cx) diff --git a/crates/git_ui/src/remote_output.rs b/crates/git_ui/src/remote_output.rs index 03fbf4f917..8437bf0d0d 100644 --- a/crates/git_ui/src/remote_output.rs +++ b/crates/git_ui/src/remote_output.rs @@ -24,7 +24,7 @@ impl RemoteAction { pub enum SuccessStyle { Toast, ToastWithLog { output: RemoteCommandOutput }, - PushPrLink { link: String }, + PushPrLink { text: String, link: String }, } pub struct SuccessMessage { @@ -37,7 +37,7 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ RemoteAction::Fetch(remote) => { if output.stderr.is_empty() { SuccessMessage { - message: "Already up to date".into(), + message: "Fetch: Already up to date".into(), style: SuccessStyle::Toast, } } else { @@ -68,10 +68,9 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ Ok(files_changed) }; - - if output.stderr.starts_with("Everything up to date") { + if output.stdout.ends_with("Already up to date.\n") { SuccessMessage { - message: output.stderr.trim().to_owned(), + message: "Pull: Already up to date".into(), style: SuccessStyle::Toast, } } else if output.stdout.starts_with("Updating") { @@ -119,48 +118,42 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ } } RemoteAction::Push(branch_name, remote_ref) => { - if output.stderr.contains("* [new branch]") { - let pr_hints = [ - // GitHub - "Create a pull request", - // Bitbucket - "Create pull request", - // GitLab - "create a merge request", - ]; - let style = if pr_hints - .iter() - .any(|indicator| output.stderr.contains(indicator)) - { - let finder = LinkFinder::new(); - let first_link = finder - .links(&output.stderr) - .filter(|link| *link.kind() == LinkKind::Url) - .map(|link| link.start()..link.end()) - .next(); - if let Some(link) = first_link { - let link = output.stderr[link].to_string(); - SuccessStyle::PushPrLink { link } - } else { - SuccessStyle::ToastWithLog { output } - } - } else { - SuccessStyle::ToastWithLog { output } - }; - SuccessMessage { - message: format!("Published {} to {}", branch_name, remote_ref.name), - style, - } - } else if output.stderr.starts_with("Everything up to date") { - SuccessMessage { - message: output.stderr.trim().to_owned(), - style: SuccessStyle::Toast, - } + let message = if output.stderr.ends_with("Everything up-to-date\n") { + "Push: Everything is up-to-date".to_string() } else { - SuccessMessage { - message: format!("Pushed {} to {}", branch_name, remote_ref.name), - style: SuccessStyle::ToastWithLog { output }, - } + format!("Pushed {} to {}", branch_name, remote_ref.name) + }; + + let style = if output.stderr.ends_with("Everything up-to-date\n") { + Some(SuccessStyle::Toast) + } else if output.stderr.contains("\nremote: ") { + let pr_hints = [ + ("Create a pull request", "Create Pull Request"), // GitHub + ("Create pull request", "Create Pull Request"), // Bitbucket + ("create a merge request", "Create Merge Request"), // GitLab + ("View merge request", "View Merge Request"), // GitLab + ]; + pr_hints + .iter() + .find(|(indicator, _)| output.stderr.contains(indicator)) + .and_then(|(_, mapped)| { + let finder = LinkFinder::new(); + finder + .links(&output.stderr) + .filter(|link| *link.kind() == LinkKind::Url) + .map(|link| link.start()..link.end()) + .next() + .map(|link| SuccessStyle::PushPrLink { + text: mapped.to_string(), + link: output.stderr[link].to_string(), + }) + }) + } else { + None + }; + SuccessMessage { + message, + style: style.unwrap_or(SuccessStyle::ToastWithLog { output }), } } } @@ -169,6 +162,7 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ #[cfg(test)] mod tests { use super::*; + use indoc::indoc; #[test] fn test_push_new_branch_pull_request() { @@ -181,8 +175,7 @@ mod tests { let output = RemoteCommandOutput { stdout: String::new(), - stderr: String::from( - " + stderr: indoc! { " Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: Create a pull request for 'test' on GitHub by visiting: @@ -190,13 +183,14 @@ mod tests { remote: To example.com:test/test.git * [new branch] test -> test - ", - ), + "} + .to_string(), }; let msg = format_output(&action, output); - if let SuccessStyle::PushPrLink { link } = &msg.style { + if let SuccessStyle::PushPrLink { text: hint, link } = &msg.style { + assert_eq!(hint, "Create Pull Request"); assert_eq!(link, "https://example.com/test/test/pull/new/test"); } else { panic!("Expected PushPrLink variant"); @@ -214,7 +208,7 @@ mod tests { let output = RemoteCommandOutput { stdout: String::new(), - stderr: String::from(" + stderr: indoc! {" Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: To create a merge request for test, visit: @@ -222,12 +216,14 @@ mod tests { remote: To example.com:test/test.git * [new branch] test -> test - "), - }; + "} + .to_string() + }; let msg = format_output(&action, output); - if let SuccessStyle::PushPrLink { link } = &msg.style { + if let SuccessStyle::PushPrLink { text, link } = &msg.style { + assert_eq!(text, "Create Merge Request"); assert_eq!( link, "https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test" @@ -237,6 +233,39 @@ mod tests { } } + #[test] + fn test_push_branch_existing_merge_request() { + let action = RemoteAction::Push( + SharedString::new("test_branch"), + Remote { + name: SharedString::new("test_remote"), + }, + ); + + let output = RemoteCommandOutput { + stdout: String::new(), + stderr: indoc! {" + Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) + remote: + remote: View merge request for test: + remote: https://example.com/test/test/-/merge_requests/99999 + remote: + To example.com:test/test.git + + 80bd3c83be...e03d499d2e test -> test + "} + .to_string(), + }; + + let msg = format_output(&action, output); + + if let SuccessStyle::PushPrLink { text, link } = &msg.style { + assert_eq!(text, "View Merge Request"); + assert_eq!(link, "https://example.com/test/test/-/merge_requests/99999"); + } else { + panic!("Expected PushPrLink variant"); + } + } + #[test] fn test_push_new_branch_no_link() { let action = RemoteAction::Push( @@ -248,12 +277,12 @@ mod tests { let output = RemoteCommandOutput { stdout: String::new(), - stderr: String::from( - " + stderr: indoc! { " To http://example.com/test/test.git * [new branch] test -> test ", - ), + } + .to_string(), }; let msg = format_output(&action, output); @@ -261,10 +290,7 @@ mod tests { if let SuccessStyle::ToastWithLog { output } = &msg.style { assert_eq!( output.stderr, - " - To http://example.com/test/test.git - * [new branch] test -> test - " + "To http://example.com/test/test.git\n * [new branch] test -> test\n" ); } else { panic!("Expected ToastWithLog variant"); From 91bbdb7002b6e8298c9876ed4e4e22b0e021b98c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:37:06 +0200 Subject: [PATCH 24/46] debugger: Install debugpy into user's venv if there's one selected (#35617) Closes #35388 Release Notes: - debugger: Fixed Python debug sessions failing to launch due to a missing debugpy installation. Debugpy is now installed into user's venv if there's one available. --- crates/dap_adapters/src/python.rs | 61 ++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index aa64fea6ed..455440d6d3 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -126,38 +126,42 @@ impl PythonDebugAdapter { } None } - + const BINARY_DIR: &str = if cfg!(target_os = "windows") { + "Scripts" + } else { + "bin" + }; async fn base_venv(&self, delegate: &dyn DapDelegate) -> Result, String> { - const BINARY_DIR: &str = if cfg!(target_os = "windows") { - "Scripts" - } else { - "bin" - }; self.python_venv_base .get_or_init(move || async move { let venv_base = Self::ensure_venv(delegate) .await .map_err(|e| format!("{e}"))?; - let pip_path = venv_base.join(BINARY_DIR).join("pip3"); - let installation_succeeded = util::command::new_smol_command(pip_path.as_path()) - .arg("install") - .arg("debugpy") - .arg("-U") - .output() - .await - .map_err(|e| format!("{e}"))? - .status - .success(); - if !installation_succeeded { - return Err("debugpy installation failed".into()); - } - + Self::install_debugpy_into_venv(&venv_base).await?; Ok(venv_base) }) .await .clone() } + async fn install_debugpy_into_venv(venv_path: &Path) -> Result<(), String> { + let pip_path = venv_path.join(Self::BINARY_DIR).join("pip3"); + let installation_succeeded = util::command::new_smol_command(pip_path.as_path()) + .arg("install") + .arg("debugpy") + .arg("-U") + .output() + .await + .map_err(|e| format!("{e}"))? + .status + .success(); + if !installation_succeeded { + return Err("debugpy installation failed".into()); + } + + Ok(()) + } + async fn get_installed_binary( &self, delegate: &Arc, @@ -629,11 +633,22 @@ impl DebugAdapter for PythonDebugAdapter { .await; } + let base_path = config + .config + .get("cwd") + .and_then(|cwd| { + cwd.as_str() + .map(Path::new)? + .strip_prefix(delegate.worktree_root_path()) + .ok() + }) + .unwrap_or_else(|| "".as_ref()) + .into(); let toolchain = delegate .toolchain_store() .active_toolchain( delegate.worktree_id(), - Arc::from("".as_ref()), + base_path, language::LanguageName::new(Self::LANGUAGE_NAME), cx, ) @@ -641,6 +656,10 @@ impl DebugAdapter for PythonDebugAdapter { if let Some(toolchain) = &toolchain { if let Some(path) = Path::new(&toolchain.path.to_string()).parent() { + if let Some(parent) = path.parent() { + Self::install_debugpy_into_venv(parent).await.ok(); + } + let debugpy_path = path.join("debugpy"); if delegate.fs().is_file(&debugpy_path).await { log::debug!( From afc4f50300024e66f8b9745a90e778c952efc6c9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:51:40 +0200 Subject: [PATCH 25/46] debugger: Ensure that Python's adapter work dir exists (#35618) Closes #ISSUE cc @Sansui233 who triaged this in https://github.com/zed-industries/zed/issues/35388#issuecomment-3146977431 Release Notes: - debugger: Fixed an issue where a Python debug adapter could not be installed when debugging Python projects for the first time. --- crates/dap_adapters/src/python.rs | 3 +++ crates/zed/src/main.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 455440d6d3..f499244966 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -101,6 +101,9 @@ impl PythonDebugAdapter { .await .context("Could not find Python installation for DebugPy")?; let work_dir = debug_adapters_dir().join(Self::ADAPTER_NAME); + if !work_dir.exists() { + std::fs::create_dir_all(&work_dir)?; + } let mut path = work_dir.clone(); path.push("debugpy-venv"); if !path.exists() { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 825aea615f..71b29909a1 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1125,6 +1125,7 @@ fn init_paths() -> HashMap> { paths::config_dir(), paths::extensions_dir(), paths::languages_dir(), + paths::debug_adapters_dir(), paths::database_dir(), paths::logs_dir(), paths::temp_dir(), From e1d0e3fc34ad6a192816719b4bb0a5af7d938283 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:52:22 -0300 Subject: [PATCH 26/46] onboarding: Add explainer tooltips for the editing and AI section (#35619) Includes the ability to add a tooltip for both the badge and switch field components. Release Notes: - N/A --- crates/onboarding/src/ai_setup_page.rs | 42 ++++++++++++- crates/onboarding/src/editing_page.rs | 85 ++++++++++++++------------ crates/ui/src/components/badge.rs | 30 ++++++++- crates/ui/src/components/toggle.rs | 82 +++++++++++++++++++++++-- 4 files changed, 191 insertions(+), 48 deletions(-) diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index b4b043196b..b5dda7601f 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -11,7 +11,7 @@ use project::DisableAiSettings; use settings::{Settings, update_settings_file}; use ui::{ Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState, - prelude::*, + prelude::*, tooltip_container, }; use util::ResultExt; use workspace::ModalView; @@ -41,7 +41,11 @@ fn render_llm_provider_section( } fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { - let privacy_badge = || Badge::new("Privacy").icon(IconName::ShieldCheck); + let privacy_badge = || { + Badge::new("Privacy") + .icon(IconName::ShieldCheck) + .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()) + }; v_flex() .relative() @@ -355,3 +359,37 @@ impl Render for AiConfigurationModal { ) } } + +pub struct AiPrivacyTooltip {} + +impl AiPrivacyTooltip { + pub fn new() -> Self { + Self {} + } +} + +impl Render for AiPrivacyTooltip { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + const DESCRIPTION: &'static str = "One of Zed's most important principles is transparency. This is why we are and value open-source so much. And it wouldn't be any different with AI."; + + tooltip_container(window, cx, move |this, _, _| { + this.child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::ShieldCheck) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(Label::new("Privacy Principle")), + ) + .child( + div().max_w_64().child( + Label::new(DESCRIPTION) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + }) + } +} diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 2972f41348..20ef17c7aa 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -9,7 +9,7 @@ use settings::{Settings as _, update_settings_file}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, - ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, prelude::*, + ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, }; use crate::{ImportCursorSettings, ImportVsCodeSettings}; @@ -357,23 +357,28 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl } fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement { + const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠."; + v_flex() .gap_5() .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) .child(render_font_customization_section(window, cx)) - .child(SwitchField::new( - "onboarding-font-ligatures", - "Font Ligatures", - Some("Combine text characters into their associated symbols.".into()), - if read_font_ligatures(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - write_font_ligatures(toggle_state == &ToggleState::Selected, cx); - }, - )) + .child( + SwitchField::new( + "onboarding-font-ligatures", + "Font Ligatures", + Some("Combine text characters into their associated symbols.".into()), + if read_font_ligatures(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_font_ligatures(toggle_state == &ToggleState::Selected, cx); + }, + ) + .tooltip(Tooltip::text(LIGATURE_TOOLTIP)), + ) .child(SwitchField::new( "onboarding-format-on-save", "Format on Save", @@ -387,6 +392,32 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In write_format_on_save(toggle_state == &ToggleState::Selected, cx); }, )) + .child(SwitchField::new( + "onboarding-enable-inlay-hints", + "Inlay Hints", + Some("See parameter names for function and method calls inline.".into()), + if read_inlay_hints(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_inlay_hints(toggle_state == &ToggleState::Selected, cx); + }, + )) + .child(SwitchField::new( + "onboarding-git-blame-switch", + "Git Blame", + Some("See who committed each line on a given file.".into()), + if read_git_blame(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + set_git_blame(toggle_state == &ToggleState::Selected, cx); + }, + )) .child( h_flex() .items_start() @@ -421,32 +452,6 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In .button_width(ui::rems_from_px(64.)), ), ) - .child(SwitchField::new( - "onboarding-enable-inlay-hints", - "Inlay Hints", - Some("See parameter names for function and method calls inline.".into()), - if read_inlay_hints(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - write_inlay_hints(toggle_state == &ToggleState::Selected, cx); - }, - )) - .child(SwitchField::new( - "onboarding-git-blame-switch", - "Git Blame", - Some("See who committed each line on a given file.".into()), - if read_git_blame(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - set_git_blame(toggle_state == &ToggleState::Selected, cx); - }, - )) } pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { diff --git a/crates/ui/src/components/badge.rs b/crates/ui/src/components/badge.rs index 2eee084bbb..f36e03291c 100644 --- a/crates/ui/src/components/badge.rs +++ b/crates/ui/src/components/badge.rs @@ -1,13 +1,18 @@ +use std::rc::Rc; + use crate::Divider; use crate::DividerColor; +use crate::Tooltip; use crate::component_prelude::*; use crate::prelude::*; +use gpui::AnyView; use gpui::{AnyElement, IntoElement, SharedString, Window}; #[derive(IntoElement, RegisterComponent)] pub struct Badge { label: SharedString, icon: IconName, + tooltip: Option AnyView>>, } impl Badge { @@ -15,6 +20,7 @@ impl Badge { Self { label: label.into(), icon: IconName::Check, + tooltip: None, } } @@ -22,11 +28,19 @@ impl Badge { self.icon = icon; self } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Rc::new(tooltip)); + self + } } impl RenderOnce for Badge { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let tooltip = self.tooltip; + h_flex() + .id(self.label.clone()) .h_full() .gap_1() .pl_1() @@ -43,6 +57,9 @@ impl RenderOnce for Badge { ) .child(Divider::vertical().color(DividerColor::Border)) .child(Label::new(self.label.clone()).size(LabelSize::Small).ml_1()) + .when_some(tooltip, |this, tooltip| { + this.tooltip(move |window, cx| tooltip(window, cx)) + }) } } @@ -59,7 +76,18 @@ impl Component for Badge { fn preview(_window: &mut Window, _cx: &mut App) -> Option { Some( - single_example("Basic Badge", Badge::new("Default").into_any_element()) + v_flex() + .gap_6() + .child(single_example( + "Basic Badge", + Badge::new("Default").into_any_element(), + )) + .child(single_example( + "With Tooltip", + Badge::new("Tooltip") + .tooltip(Tooltip::text("This is a tooltip.")) + .into_any_element(), + )) .into_any_element(), ) } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 0d8f5c4107..a3a3f23889 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -2,10 +2,10 @@ use gpui::{ AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, prelude::*, }; -use std::sync::Arc; +use std::{rc::Rc, sync::Arc}; use crate::utils::is_light; -use crate::{Color, Icon, IconName, ToggleState}; +use crate::{Color, Icon, IconName, ToggleState, Tooltip}; use crate::{ElevationIndex, KeyBinding, prelude::*}; // TODO: Checkbox, CheckboxWithLabel, and Switch could all be @@ -571,6 +571,7 @@ pub struct SwitchField { on_click: Arc, disabled: bool, color: SwitchColor, + tooltip: Option AnyView>>, } impl SwitchField { @@ -589,6 +590,7 @@ impl SwitchField { on_click: Arc::new(on_click), disabled: false, color: SwitchColor::Accent, + tooltip: None, } } @@ -608,10 +610,17 @@ impl SwitchField { self.color = color; self } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Rc::new(tooltip)); + self + } } impl RenderOnce for SwitchField { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let tooltip = self.tooltip; + h_flex() .id(SharedString::from(format!("{}-container", self.id))) .when(!self.disabled, |this| { @@ -621,14 +630,48 @@ impl RenderOnce for SwitchField { .gap_4() .justify_between() .flex_wrap() - .child(match &self.description { - Some(description) => v_flex() + .child(match (&self.description, &tooltip) { + (Some(description), Some(tooltip)) => v_flex() + .gap_0p5() + .max_w_5_6() + .child( + h_flex() + .gap_0p5() + .child(Label::new(self.label.clone())) + .child( + IconButton::new("tooltip_button", IconName::Info) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .shape(crate::IconButtonShape::Square) + .tooltip({ + let tooltip = tooltip.clone(); + move |window, cx| tooltip(window, cx) + }), + ), + ) + .child(Label::new(description.clone()).color(Color::Muted)) + .into_any_element(), + (Some(description), None) => v_flex() .gap_0p5() .max_w_5_6() .child(Label::new(self.label.clone())) .child(Label::new(description.clone()).color(Color::Muted)) .into_any_element(), - None => Label::new(self.label.clone()).into_any_element(), + (None, Some(tooltip)) => h_flex() + .gap_0p5() + .child(Label::new(self.label.clone())) + .child( + IconButton::new("tooltip_button", IconName::Info) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .shape(crate::IconButtonShape::Square) + .tooltip({ + let tooltip = tooltip.clone(); + move |window, cx| tooltip(window, cx) + }), + ) + .into_any_element(), + (None, None) => Label::new(self.label.clone()).into_any_element(), }) .child( Switch::new( @@ -754,6 +797,35 @@ impl Component for SwitchField { .into_any_element(), )], ), + example_group_with_title( + "With Tooltip", + vec![ + single_example( + "Tooltip with Description", + SwitchField::new( + "switch_field_tooltip_with_desc", + "Nice Feature", + Some("Enable advanced configuration options.".into()), + ToggleState::Unselected, + |_, _, _| {}, + ) + .tooltip(Tooltip::text("This is content for this tooltip!")) + .into_any_element(), + ), + single_example( + "Tooltip without Description", + SwitchField::new( + "switch_field_tooltip_no_desc", + "Nice Feature", + None, + ToggleState::Selected, + |_, _, _| {}, + ) + .tooltip(Tooltip::text("This is content for this tooltip!")) + .into_any_element(), + ), + ], + ), ]) .into_any_element(), ) From 06226e1cbd00eac371cc5d9c291a551881317744 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 4 Aug 2025 19:01:53 -0500 Subject: [PATCH 27/46] onboarding: Show indication that settings have already been imported (#35615) Co-Authored-By: Danilo Co-Authored-By: Anthony Release Notes: - N/A --- Cargo.lock | 1 + crates/onboarding/Cargo.toml | 1 + crates/onboarding/src/editing_page.rs | 128 ++++++++++++++------------ crates/onboarding/src/onboarding.rs | 79 ++++++++++++++-- crates/settings/src/settings_store.rs | 73 ++++++++++----- 5 files changed, 192 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69386b3020..2ef41eafc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10929,6 +10929,7 @@ dependencies = [ "language", "language_model", "menu", + "notifications", "project", "schemars", "serde", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 8f684dd1b8..b3056ff39e 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -30,6 +30,7 @@ itertools.workspace = true language.workspace = true language_model.workspace = true menu.workspace = true +notifications.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 20ef17c7aa..a5e3a6bf05 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -12,7 +12,7 @@ use ui::{ ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, }; -use crate::{ImportCursorSettings, ImportVsCodeSettings}; +use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState}; fn read_show_mini_map(cx: &App) -> ShowMinimap { editor::EditorSettings::get_global(cx).minimap.show @@ -165,7 +165,71 @@ fn write_format_on_save(format_on_save: bool, cx: &mut App) { }); } -fn render_import_settings_section() -> impl IntoElement { +fn render_setting_import_button( + label: SharedString, + icon_name: IconName, + action: &dyn Action, + imported: bool, +) -> impl IntoElement { + let action = action.boxed_clone(); + h_flex().w_full().child( + ButtonLike::new(label.clone()) + .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Large) + .child( + h_flex() + .w_full() + .justify_between() + .child( + h_flex() + .gap_1p5() + .px_1() + .child( + Icon::new(icon_name) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child(Label::new(label)), + ) + .when(imported, |this| { + this.child( + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::Check) + .color(Color::Success) + .size(IconSize::XSmall), + ) + .child(Label::new("Imported").size(LabelSize::Small)), + ) + }), + ) + .on_click(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), + ) +} + +fn render_import_settings_section(cx: &App) -> impl IntoElement { + let import_state = SettingsImportState::global(cx); + let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [ + ( + "VS Code".into(), + IconName::EditorVsCode, + &ImportVsCodeSettings { skip_prompt: false }, + import_state.vscode, + ), + ( + "Cursor".into(), + IconName::EditorCursor, + &ImportCursorSettings { skip_prompt: false }, + import_state.cursor, + ), + ]; + + let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| { + render_setting_import_button(label, icon_name, action, imported) + }); + v_flex() .gap_4() .child( @@ -176,63 +240,7 @@ fn render_import_settings_section() -> impl IntoElement { .color(Color::Muted), ), ) - .child( - h_flex() - .w_full() - .gap_4() - .child( - h_flex().w_full().child( - ButtonLike::new("import_vs_code") - .full_width() - .style(ButtonStyle::Outlined) - .size(ButtonSize::Large) - .child( - h_flex() - .w_full() - .gap_1p5() - .px_1() - .child( - Icon::new(IconName::EditorVsCode) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new("VS Code")), - ) - .on_click(|_, window, cx| { - window.dispatch_action( - ImportVsCodeSettings::default().boxed_clone(), - cx, - ) - }), - ), - ) - .child( - h_flex().w_full().child( - ButtonLike::new("import_cursor") - .full_width() - .style(ButtonStyle::Outlined) - .size(ButtonSize::Large) - .child( - h_flex() - .w_full() - .gap_1p5() - .px_1() - .child( - Icon::new(IconName::EditorCursor) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new("Cursor")), - ) - .on_click(|_, window, cx| { - window.dispatch_action( - ImportCursorSettings::default().boxed_clone(), - cx, - ) - }), - ), - ), - ) + .child(h_flex().w_full().gap_4().child(vscode).child(cursor)) } fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { @@ -457,6 +465,6 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { v_flex() .gap_4() - .child(render_import_settings_section()) + .child(render_import_settings_section(cx)) .child(render_popular_settings_section(window, cx)) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index a79d1d5aef..42e75ac2f8 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -6,9 +6,10 @@ use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; use fs::Fs; use gpui::{ Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, IntoElement, KeyContext, Render, SharedString, Subscription, Task, - WeakEntity, Window, actions, + FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, SharedString, Subscription, + Task, WeakEntity, Window, actions, }; +use notifications::status_toast::{StatusToast, ToastIcon}; use schemars::JsonSchema; use serde::Deserialize; use settings::{SettingsStore, VsCodeSettingsSource}; @@ -137,9 +138,12 @@ pub fn init(cx: &mut App) { let fs = ::global(cx); let action = *action; + let workspace = cx.weak_entity(); + window .spawn(cx, async move |cx: &mut AsyncWindowContext| { handle_import_vscode_settings( + workspace, VsCodeSettingsSource::VsCode, action.skip_prompt, fs, @@ -154,9 +158,12 @@ pub fn init(cx: &mut App) { let fs = ::global(cx); let action = *action; + let workspace = cx.weak_entity(); + window .spawn(cx, async move |cx: &mut AsyncWindowContext| { handle_import_vscode_settings( + workspace, VsCodeSettingsSource::Cursor, action.skip_prompt, fs, @@ -555,6 +562,7 @@ impl Item for Onboarding { } pub async fn handle_import_vscode_settings( + workspace: WeakEntity, source: VsCodeSettingsSource, skip_prompt: bool, fs: Arc, @@ -595,14 +603,73 @@ pub async fn handle_import_vscode_settings( } }; - cx.update(|_, cx| { + let Ok(result_channel) = cx.update(|_, cx| { let source = vscode_settings.source; let path = vscode_settings.path.clone(); - cx.global::() + let result_channel = cx + .global::() .import_vscode_settings(fs, vscode_settings); zlog::info!("Imported {source} settings from {}", path.display()); - }) - .ok(); + result_channel + }) else { + return; + }; + + let result = result_channel.await; + workspace + .update_in(cx, |workspace, _, cx| match result { + Ok(_) => { + let confirmation_toast = StatusToast::new( + format!("Your {} settings were successfully imported.", source), + cx, + |this, _| { + this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) + .dismiss_button(true) + }, + ); + SettingsImportState::update(cx, |state, _| match source { + VsCodeSettingsSource::VsCode => { + state.vscode = true; + } + VsCodeSettingsSource::Cursor => { + state.cursor = true; + } + }); + workspace.toggle_status_toast(confirmation_toast, cx); + } + Err(_) => { + let error_toast = StatusToast::new( + "Failed to import settings. See log for details", + cx, + |this, _| { + this.icon(ToastIcon::new(IconName::X).color(Color::Error)) + .action("Open Log", |window, cx| { + window.dispatch_action(workspace::OpenLog.boxed_clone(), cx) + }) + .dismiss_button(true) + }, + ); + workspace.toggle_status_toast(error_toast, cx); + } + }) + .ok(); +} + +#[derive(Default, Copy, Clone)] +pub struct SettingsImportState { + pub cursor: bool, + pub vscode: bool, +} + +impl Global for SettingsImportState {} + +impl SettingsImportState { + pub fn global(cx: &App) -> Self { + cx.try_global().cloned().unwrap_or_default() + } + pub fn update(cx: &mut App, f: impl FnOnce(&mut Self, &mut App) -> R) -> R { + cx.update_default_global(f) + } } impl workspace::SerializableItem for Onboarding { diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 7f6437dac8..bc42d2c886 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -2,7 +2,11 @@ use anyhow::{Context as _, Result}; use collections::{BTreeMap, HashMap, btree_map, hash_map}; use ec4rs::{ConfigParser, PropertiesSource, Section}; use fs::Fs; -use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture}; +use futures::{ + FutureExt, StreamExt, + channel::{mpsc, oneshot}, + future::LocalBoxFuture, +}; use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; @@ -531,39 +535,60 @@ impl SettingsStore { .ok(); } - pub fn import_vscode_settings(&self, fs: Arc, vscode_settings: VsCodeSettings) { + pub fn import_vscode_settings( + &self, + fs: Arc, + vscode_settings: VsCodeSettings, + ) -> oneshot::Receiver> { + let (tx, rx) = oneshot::channel::>(); self.setting_file_updates_tx .unbounded_send(Box::new(move |cx: AsyncApp| { async move { - let old_text = Self::load_settings(&fs).await?; - let new_text = cx.read_global(|store: &SettingsStore, _cx| { - store.get_vscode_edits(old_text, &vscode_settings) - })?; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - let resolved_path = - fs.canonicalize(settings_path).await.with_context(|| { - format!("Failed to canonicalize settings path {:?}", settings_path) - })?; + let res = async move { + let old_text = Self::load_settings(&fs).await?; + let new_text = cx.read_global(|store: &SettingsStore, _cx| { + store.get_vscode_edits(old_text, &vscode_settings) + })?; + let settings_path = paths::settings_file().as_path(); + if fs.is_file(settings_path).await { + let resolved_path = + fs.canonicalize(settings_path).await.with_context(|| { + format!( + "Failed to canonicalize settings path {:?}", + settings_path + ) + })?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", resolved_path) - })?; - } else { - fs.atomic_write(settings_path.to_path_buf(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", settings_path) - })?; + fs.atomic_write(resolved_path.clone(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", resolved_path) + })?; + } else { + fs.atomic_write(settings_path.to_path_buf(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", settings_path) + })?; + } + + anyhow::Ok(()) } + .await; - anyhow::Ok(()) + let new_res = match &res { + Ok(_) => anyhow::Ok(()), + Err(e) => Err(anyhow::anyhow!("Failed to write settings to file {:?}", e)), + }; + + _ = tx.send(new_res); + res } .boxed_local() })) .ok(); + + rx } } From a9c44ac5511dc55239c7d125252a613e872844d0 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 5 Aug 2025 00:32:42 +0000 Subject: [PATCH 28/46] assistant_tool: Fix rejecting edits deletes newly created and accepted files (#35622) Closes #34108 Closes #33234 This PR fixes a bug where a file remained in a Created state after accept, causing following reject actions to incorrectly delete the file instead of reverting back to previous state. Now it changes it to Modified state upon "Accept All" and "Accept Hunk" (when all edits are accepted). - [x] Tests Release Notes: - Fixed issue where rejecting AI edits on newly created files would delete the file instead of reverting to previous accepted state. --- crates/assistant_tool/src/action_log.rs | 136 ++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs index 672c048872..025aba060d 100644 --- a/crates/assistant_tool/src/action_log.rs +++ b/crates/assistant_tool/src/action_log.rs @@ -630,6 +630,11 @@ impl ActionLog { false } }); + if tracked_buffer.unreviewed_edits.is_empty() { + if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status { + tracked_buffer.status = TrackedBufferStatus::Modified; + } + } tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); } } @@ -775,6 +780,9 @@ impl ActionLog { .retain(|_buffer, tracked_buffer| match tracked_buffer.status { TrackedBufferStatus::Deleted => false, _ => { + if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status { + tracked_buffer.status = TrackedBufferStatus::Modified; + } tracked_buffer.unreviewed_edits.clear(); tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone(); tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); @@ -2075,6 +2083,134 @@ mod tests { assert_eq!(content, "ai content\nuser added this line"); } + #[gpui::test] + async fn test_reject_after_accepting_hunk_on_created_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + let file_path = project + .read_with(cx, |project, cx| { + project.find_project_path("dir/new_file", cx) + }) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) + .await + .unwrap(); + + // AI creates file with initial content + cx.update(|cx| { + action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); + buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx)); + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + cx.run_until_parked(); + assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); + + // User accepts the single hunk + action_log.update(cx, |log, cx| { + log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, cx) + }); + cx.run_until_parked(); + assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); + assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); + + // AI modifies the file + cx.update(|cx| { + buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx)); + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + cx.run_until_parked(); + assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); + + // User rejects the hunk + action_log + .update(cx, |log, cx| { + log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], cx) + }) + .await + .unwrap(); + cx.run_until_parked(); + assert!(fs.is_file(path!("/dir/new_file").as_ref()).await,); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.text()), + "ai content v1" + ); + assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); + } + + #[gpui::test] + async fn test_reject_edits_on_previously_accepted_created_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + let file_path = project + .read_with(cx, |project, cx| { + project.find_project_path("dir/new_file", cx) + }) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) + .await + .unwrap(); + + // AI creates file with initial content + cx.update(|cx| { + action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); + buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx)); + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + cx.run_until_parked(); + + // User clicks "Accept All" + action_log.update(cx, |log, cx| log.keep_all_edits(cx)); + cx.run_until_parked(); + assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); + assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared + + // AI modifies file again + cx.update(|cx| { + buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx)); + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + cx.run_until_parked(); + assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); + + // User clicks "Reject All" + action_log + .update(cx, |log, cx| log.reject_all_edits(cx)) + .await; + cx.run_until_parked(); + assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.text()), + "ai content v1" + ); + assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); + } + #[gpui::test(iterations = 100)] async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) { init_test(cx); From be2f54b2339b502cd6aee6a3b03b549768f9a9b0 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 21:36:43 -0300 Subject: [PATCH 29/46] agent: Update pieces of copy in the settings view (#35621) Some tiny updates to make the agent panel's copywriting sharper. Release Notes: - N/A --- crates/agent_ui/src/agent_configuration.rs | 2 +- crates/language_models/src/provider/anthropic.rs | 2 +- crates/language_models/src/provider/bedrock.rs | 2 +- crates/language_models/src/provider/copilot_chat.rs | 3 ++- crates/language_models/src/provider/google.rs | 2 +- crates/language_models/src/provider/mistral.rs | 2 +- crates/language_models/src/provider/ollama.rs | 2 +- crates/language_models/src/provider/open_ai.rs | 2 +- crates/language_models/src/provider/open_ai_compatible.rs | 2 +- crates/language_models/src/provider/open_router.rs | 2 +- 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index dad930be9e..02c15b7e41 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -539,7 +539,7 @@ impl AgentConfiguration { v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) - .child(Label::new("Connect to context servers via the Model Context Protocol either via Zed extensions or directly.").color(Color::Muted)), + .child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)), ) .children( context_server_ids.into_iter().map(|context_server_id| { diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 959cbccf39..ef21e85f71 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -1012,7 +1012,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with Anthropic, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with Anthropic, you need to add an API key. Follow these steps:")) .child( List::new() .child( diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index a86b3e78f5..6df96c5c56 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -1251,7 +1251,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(ConfigurationView::save_credentials)) - .child(Label::new("To use Zed's assistant with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials.")) + .child(Label::new("To use Zed's agent with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials.")) .child(Label::new("But, to access models on AWS, you need to:").mt_1()) .child( List::new() diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index 3cdc2e5401..73f73a9a31 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -706,7 +706,8 @@ impl Render for ConfigurationView { .child(svg().size_8().path(IconName::CopilotError.path())) } _ => { - const LABEL: &str = "To use Zed's assistant with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; + const LABEL: &str = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; + v_flex().gap_2().child(Label::new(LABEL)).child( Button::new("sign_in", "Sign in to use GitHub Copilot") .icon_color(Color::Muted) diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index bd8a09970a..b287e8181a 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -880,7 +880,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with Google AI, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with Google AI, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index fb385308fa..02e53cb99a 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -807,7 +807,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with Mistral, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with Mistral, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index d4739bcab8..c845c97b09 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -631,7 +631,7 @@ impl Render for ConfigurationView { } }) .child( - Button::new("view-models", "All Models") + Button::new("view-models", "View All Models") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) .icon_size(IconSize::XSmall) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 6c4d4c9b3e..79ef4a0ee0 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -780,7 +780,7 @@ impl Render for ConfigurationView { let api_key_section = if self.should_render_editor(cx) { v_flex() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with OpenAI, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with OpenAI, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 64add5483d..38bd7cee06 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -466,7 +466,7 @@ impl Render for ConfigurationView { let api_key_section = if self.should_render_editor(cx) { v_flex() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with an OpenAI compatible provider, you need to add an API key.")) + .child(Label::new("To use Zed's agent with an OpenAI-compatible provider, you need to add an API key.")) .child( div() .pt(DynamicSpacing::Base04.rems(cx)) diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 5a6acc4329..3a492086f1 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -855,7 +855,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with OpenRouter, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with OpenRouter, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( From 07e3d53d58b5fad7d267d71556a96ffd41b4108e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 02:37:22 +0200 Subject: [PATCH 30/46] sum_tree: Do not implement Dimension on tuples, use new Dimensions wrapper instead (#35482) This is a bit of a readability improvement IMHO; I often find myself confused when dealing when dimension pairs, as there's no easy way to jump to the implementation of a dimension for tuples to remind myself for the n-th time how exactly that impl works. Now it should be possible to jump directly to that impl. Another bonus is that Dimension supports 3-ary tuples as well - by using a () as a default value of a 3rd dimension. Release Notes: - N/A --- Cargo.lock | 1 + crates/channel/src/channel_chat.rs | 10 ++-- crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 3 +- crates/editor/src/display_map/block_map.rs | 42 ++++++++-------- crates/editor/src/display_map/fold_map.rs | 43 ++++++++++------ crates/editor/src/display_map/inlay_map.rs | 50 +++++++++++-------- crates/editor/src/display_map/wrap_map.rs | 38 ++++++++++---- crates/gpui/src/elements/list.rs | 6 +-- crates/language/src/syntax_map.rs | 11 ++-- crates/multi_buffer/src/multi_buffer.rs | 47 +++++++++-------- .../notifications/src/notification_store.rs | 6 ++- crates/project/src/lsp_store.rs | 5 +- crates/rope/src/rope.rs | 25 +++++----- crates/sum_tree/src/sum_tree.rs | 27 ++++------ crates/text/src/anchor.rs | 6 ++- crates/text/src/text.rs | 24 ++++++--- crates/worktree/src/worktree.rs | 11 ++-- 18 files changed, 215 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ef41eafc5..6239c83fdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3562,6 +3562,7 @@ dependencies = [ "serde", "serde_json", "settings", + "sum_tree", "task", "theme", "ui", diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 866e3ccd90..4ac37ffd14 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -13,7 +13,7 @@ use std::{ ops::{ControlFlow, Range}, sync::Arc, }; -use sum_tree::{Bias, SumTree}; +use sum_tree::{Bias, Dimensions, SumTree}; use time::OffsetDateTime; use util::{ResultExt as _, TryFutureExt, post_inc}; @@ -331,7 +331,9 @@ impl ChannelChat { .update(&mut cx, |chat, cx| { if let Some(first_id) = chat.first_loaded_message_id() { if first_id <= message_id { - let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(&()); + let mut cursor = chat + .messages + .cursor::>(&()); let message_id = ChannelMessageId::Saved(message_id); cursor.seek(&message_id, Bias::Left); return ControlFlow::Break( @@ -587,7 +589,9 @@ impl ChannelChat { .map(|m| m.nonce) .collect::>(); - let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>(&()); + let mut old_cursor = self + .messages + .cursor::>(&()); let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left); let start_ix = old_cursor.start().1.0; let removed_messages = old_cursor.slice(&last_message.id, Bias::Right); diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 8908143324..0fc119f311 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -46,6 +46,7 @@ project.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +sum_tree.workspace = true task.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index efe6fb743a..49ae2b9d9c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -39,6 +39,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use sum_tree::Dimensions; use util::{ResultExt, fs::remove_matching}; use workspace::Workspace; @@ -239,7 +240,7 @@ impl RegisteredBuffer { let new_snapshot = new_snapshot.clone(); async move { new_snapshot - .edits_since::<(PointUtf16, usize)>(&old_version) + .edits_since::>(&old_version) .map(|edit| { let edit_start = edit.new.start.0; let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 85495a2611..e25c02432d 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -22,7 +22,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, }, }; -use sum_tree::{Bias, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Dimensions, SumTree, Summary, TreeMap}; use text::{BufferId, Edit}; use ui::ElementId; @@ -416,7 +416,7 @@ struct TransformSummary { } pub struct BlockChunks<'a> { - transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + transforms: sum_tree::Cursor<'a, Transform, Dimensions>, input_chunks: wrap_map::WrapChunks<'a>, input_chunk: Chunk<'a>, output_row: u32, @@ -426,7 +426,7 @@ pub struct BlockChunks<'a> { #[derive(Clone)] pub struct BlockRows<'a> { - transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + transforms: sum_tree::Cursor<'a, Transform, Dimensions>, input_rows: wrap_map::WrapRows<'a>, output_row: BlockRow, started: bool, @@ -970,7 +970,7 @@ impl BlockMapReader<'_> { .unwrap_or(self.wrap_snapshot.max_point().row() + 1), ); - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&start_wrap_row, Bias::Left); while let Some(transform) = cursor.item() { if cursor.start().0 > end_wrap_row { @@ -1292,7 +1292,7 @@ impl BlockSnapshot { ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&BlockRow(rows.start), Bias::Right); let transform_output_start = cursor.start().0.0; let transform_input_start = cursor.start().1.0; @@ -1324,9 +1324,9 @@ impl BlockSnapshot { } pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&start_row, Bias::Right); - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = if cursor .item() .map_or(false, |transform| transform.block.is_none()) @@ -1441,14 +1441,14 @@ impl BlockSnapshot { } pub fn longest_row_in_range(&self, range: Range) -> BlockRow { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&range.start, Bias::Right); let mut longest_row = range.start; let mut longest_row_chars = 0; if let Some(transform) = cursor.item() { if transform.block.is_none() { - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = range.start.0 - output_start.0; let wrap_start_row = input_start.0 + overshoot; let wrap_end_row = cmp::min( @@ -1474,7 +1474,7 @@ impl BlockSnapshot { if let Some(transform) = cursor.item() { if transform.block.is_none() { - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = range.end.0 - output_start.0; let wrap_start_row = input_start.0; let wrap_end_row = input_start.0 + overshoot; @@ -1492,10 +1492,10 @@ impl BlockSnapshot { } pub(super) fn line_len(&self, row: BlockRow) -> u32 { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&BlockRow(row.0), Bias::Right); if let Some(transform) = cursor.item() { - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = row.0 - output_start.0; if transform.block.is_some() { 0 @@ -1510,13 +1510,13 @@ impl BlockSnapshot { } pub(super) fn is_block_line(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&row, Bias::Right); cursor.item().map_or(false, |t| t.block.is_some()) } pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&row, Bias::Right); let Some(transform) = cursor.item() else { return false; @@ -1528,7 +1528,7 @@ impl BlockSnapshot { let wrap_point = self .wrap_snapshot .make_wrap_point(Point::new(row.0, 0), Bias::Left); - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); cursor.item().map_or(false, |transform| { transform @@ -1539,7 +1539,7 @@ impl BlockSnapshot { } pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&BlockRow(point.row), Bias::Right); let max_input_row = WrapRow(self.transforms.summary().input_rows); @@ -1549,8 +1549,8 @@ impl BlockSnapshot { loop { if let Some(transform) = cursor.item() { - let (output_start_row, input_start_row) = cursor.start(); - let (output_end_row, input_end_row) = cursor.end(); + let Dimensions(output_start_row, input_start_row, _) = cursor.start(); + let Dimensions(output_end_row, input_end_row, _) = cursor.end(); let output_start = Point::new(output_start_row.0, 0); let input_start = Point::new(input_start_row.0, 0); let input_end = Point::new(input_end_row.0, 0); @@ -1599,13 +1599,13 @@ impl BlockSnapshot { } pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint { - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); if let Some(transform) = cursor.item() { if transform.block.is_some() { BlockPoint::new(cursor.start().1.0, 0) } else { - let (input_start_row, output_start_row) = cursor.start(); + let Dimensions(input_start_row, output_start_row, _) = cursor.start(); let input_start = Point::new(input_start_row.0, 0); let output_start = Point::new(output_start_row.0, 0); let input_overshoot = wrap_point.0 - input_start; @@ -1617,7 +1617,7 @@ impl BlockSnapshot { } pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&BlockRow(block_point.row), Bias::Right); if let Some(transform) = cursor.item() { match transform.block.as_ref() { diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 829d34ff58..c4e53a0f43 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -17,7 +17,7 @@ use std::{ sync::Arc, usize, }; -use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Cursor, Dimensions, FilterCursor, SumTree, Summary, TreeMap}; use ui::IntoElement as _; use util::post_inc; @@ -98,7 +98,9 @@ impl FoldPoint { } pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint { - let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); + let mut cursor = snapshot + .transforms + .cursor::>(&()); cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().0.0; InlayPoint(cursor.start().1.0 + overshoot) @@ -107,7 +109,7 @@ impl FoldPoint { pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset { let mut cursor = snapshot .transforms - .cursor::<(FoldPoint, TransformSummary)>(&()); + .cursor::>(&()); cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().1.output.lines; let mut offset = cursor.start().1.output.len; @@ -567,8 +569,9 @@ impl FoldMap { let mut old_transforms = self .snapshot .transforms - .cursor::<(InlayOffset, FoldOffset)>(&()); - let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>(&()); + .cursor::>(&()); + let mut new_transforms = + new_transforms.cursor::>(&()); for mut edit in inlay_edits { old_transforms.seek(&edit.old.start, Bias::Left); @@ -651,7 +654,9 @@ impl FoldSnapshot { pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&range.start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = range.start.0 - cursor.start().0.0; @@ -700,7 +705,9 @@ impl FoldSnapshot { } pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint { - let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); if cursor.item().map_or(false, |t| t.is_fold()) { if bias == Bias::Left || point == cursor.start().0 { @@ -734,7 +741,9 @@ impl FoldSnapshot { } let fold_point = FoldPoint::new(start_row, 0); - let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&fold_point, Bias::Left); let overshoot = fold_point.0 - cursor.start().0.0; @@ -816,7 +825,9 @@ impl FoldSnapshot { language_aware: bool, highlights: Highlights<'a>, ) -> FoldChunks<'a> { - let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); + let mut transform_cursor = self + .transforms + .cursor::>(&()); transform_cursor.seek(&range.start, Bias::Right); let inlay_start = { @@ -871,7 +882,9 @@ impl FoldSnapshot { } pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint { - let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); if let Some(transform) = cursor.item() { let transform_start = cursor.start().0.0; @@ -1196,7 +1209,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize { #[derive(Clone)] pub struct FoldRows<'a> { - cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>, + cursor: Cursor<'a, Transform, Dimensions>, input_rows: InlayBufferRows<'a>, fold_point: FoldPoint, } @@ -1313,7 +1326,7 @@ impl DerefMut for ChunkRendererContext<'_, '_> { } pub struct FoldChunks<'a> { - transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>, + transform_cursor: Cursor<'a, Transform, Dimensions>, inlay_chunks: InlayChunks<'a>, inlay_chunk: Option<(InlayOffset, InlayChunk<'a>)>, inlay_offset: InlayOffset, @@ -1448,7 +1461,7 @@ impl FoldOffset { pub fn to_point(self, snapshot: &FoldSnapshot) -> FoldPoint { let mut cursor = snapshot .transforms - .cursor::<(FoldOffset, TransformSummary)>(&()); + .cursor::>(&()); cursor.seek(&self, Bias::Right); let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) { Point::new(0, (self.0 - cursor.start().0.0) as u32) @@ -1462,7 +1475,9 @@ impl FoldOffset { #[cfg(test)] pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset { - let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); + let mut cursor = snapshot + .transforms + .cursor::>(&()); cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().0.0; InlayOffset(cursor.start().1.0 + overshoot) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 0b1c7a4bed..fd49c262c6 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -10,7 +10,7 @@ use std::{ ops::{Add, AddAssign, Range, Sub, SubAssign}, sync::Arc, }; -use sum_tree::{Bias, Cursor, SumTree}; +use sum_tree::{Bias, Cursor, Dimensions, SumTree}; use text::{Patch, Rope}; use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div}; @@ -235,14 +235,14 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { #[derive(Clone)] pub struct InlayBufferRows<'a> { - transforms: Cursor<'a, Transform, (InlayPoint, Point)>, + transforms: Cursor<'a, Transform, Dimensions>, buffer_rows: MultiBufferRows<'a>, inlay_row: u32, max_buffer_row: MultiBufferRow, } pub struct InlayChunks<'a> { - transforms: Cursor<'a, Transform, (InlayOffset, usize)>, + transforms: Cursor<'a, Transform, Dimensions>, buffer_chunks: CustomHighlightsChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, @@ -551,7 +551,9 @@ impl InlayMap { } else { let mut inlay_edits = Patch::default(); let mut new_transforms = SumTree::default(); - let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(&()); + let mut cursor = snapshot + .transforms + .cursor::>(&()); let mut buffer_edits_iter = buffer_edits.iter().peekable(); while let Some(buffer_edit) = buffer_edits_iter.next() { new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left), &()); @@ -770,20 +772,20 @@ impl InlaySnapshot { pub fn to_point(&self, offset: InlayOffset) -> InlayPoint { let mut cursor = self .transforms - .cursor::<(InlayOffset, (InlayPoint, usize))>(&()); + .cursor::>(&()); cursor.seek(&offset, Bias::Right); let overshoot = offset.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { - let buffer_offset_start = cursor.start().1.1; + let buffer_offset_start = cursor.start().2; let buffer_offset_end = buffer_offset_start + overshoot; let buffer_start = self.buffer.offset_to_point(buffer_offset_start); let buffer_end = self.buffer.offset_to_point(buffer_offset_end); - InlayPoint(cursor.start().1.0.0 + (buffer_end - buffer_start)) + InlayPoint(cursor.start().1.0 + (buffer_end - buffer_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.offset_to_point(overshoot); - InlayPoint(cursor.start().1.0.0 + overshoot) + InlayPoint(cursor.start().1.0 + overshoot) } None => self.max_point(), } @@ -800,26 +802,26 @@ impl InlaySnapshot { pub fn to_offset(&self, point: InlayPoint) -> InlayOffset { let mut cursor = self .transforms - .cursor::<(InlayPoint, (InlayOffset, Point))>(&()); + .cursor::>(&()); cursor.seek(&point, Bias::Right); let overshoot = point.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { - let buffer_point_start = cursor.start().1.1; + let buffer_point_start = cursor.start().2; let buffer_point_end = buffer_point_start + overshoot; let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start); let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end); - InlayOffset(cursor.start().1.0.0 + (buffer_offset_end - buffer_offset_start)) + InlayOffset(cursor.start().1.0 + (buffer_offset_end - buffer_offset_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.point_to_offset(overshoot); - InlayOffset(cursor.start().1.0.0 + overshoot) + InlayOffset(cursor.start().1.0 + overshoot) } None => self.len(), } } pub fn to_buffer_point(&self, point: InlayPoint) -> Point { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&point, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { @@ -831,7 +833,9 @@ impl InlaySnapshot { } } pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&offset, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { @@ -844,7 +848,9 @@ impl InlaySnapshot { } pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset { - let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&offset, Bias::Left); loop { match cursor.item() { @@ -877,7 +883,7 @@ impl InlaySnapshot { } } pub fn to_inlay_point(&self, point: Point) -> InlayPoint { - let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&point, Bias::Left); loop { match cursor.item() { @@ -911,7 +917,7 @@ impl InlaySnapshot { } pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&point, Bias::Left); loop { match cursor.item() { @@ -1008,7 +1014,9 @@ impl InlaySnapshot { pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&range.start, Bias::Right); let overshoot = range.start.0 - cursor.start().0.0; @@ -1056,7 +1064,7 @@ impl InlaySnapshot { } pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + let mut cursor = self.transforms.cursor::>(&()); let inlay_point = InlayPoint::new(row, 0); cursor.seek(&inlay_point, Bias::Left); @@ -1098,7 +1106,9 @@ impl InlaySnapshot { language_aware: bool, highlights: Highlights<'a>, ) -> InlayChunks<'a> { - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&range.start, Bias::Right); let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index d55577826e..269f8f0c40 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -9,7 +9,7 @@ use multi_buffer::{MultiBufferSnapshot, RowInfo}; use smol::future::yield_now; use std::sync::LazyLock; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; -use sum_tree::{Bias, Cursor, SumTree}; +use sum_tree::{Bias, Cursor, Dimensions, SumTree}; use text::Patch; pub use super::tab_map::TextSummary; @@ -55,7 +55,7 @@ pub struct WrapChunks<'a> { input_chunk: Chunk<'a>, output_position: WrapPoint, max_output_row: u32, - transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, + transforms: Cursor<'a, Transform, Dimensions>, snapshot: &'a WrapSnapshot, } @@ -66,7 +66,7 @@ pub struct WrapRows<'a> { output_row: u32, soft_wrapped: bool, max_output_row: u32, - transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, + transforms: Cursor<'a, Transform, Dimensions>, } impl WrapRows<'_> { @@ -598,7 +598,9 @@ impl WrapSnapshot { ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); - let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut transforms = self + .transforms + .cursor::>(&()); transforms.seek(&output_start, Bias::Right); let mut input_start = TabPoint(transforms.start().1.0); if transforms.item().map_or(false, |t| t.is_isomorphic()) { @@ -626,7 +628,9 @@ impl WrapSnapshot { } pub fn line_len(&self, row: u32) -> u32 { - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left); if cursor .item() @@ -651,7 +655,9 @@ impl WrapSnapshot { let start = WrapPoint::new(rows.start, 0); let end = WrapPoint::new(rows.end, 0); - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = start.0 - cursor.start().0.0; @@ -721,7 +727,9 @@ impl WrapSnapshot { } pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> { - let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut transforms = self + .transforms + .cursor::>(&()); transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left); let mut input_row = transforms.start().1.row(); if transforms.item().map_or(false, |t| t.is_isomorphic()) { @@ -741,7 +749,9 @@ impl WrapSnapshot { } pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint { - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); let mut tab_point = cursor.start().1.0; if cursor.item().map_or(false, |t| t.is_isomorphic()) { @@ -759,7 +769,9 @@ impl WrapSnapshot { } pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint { - let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); WrapPoint(cursor.start().1.0 + (point.0 - cursor.start().0.0)) } @@ -784,7 +796,9 @@ impl WrapSnapshot { *point.column_mut() = 0; - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); if cursor.item().is_none() { cursor.prev(); @@ -804,7 +818,9 @@ impl WrapSnapshot { pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option { point.0 += Point::new(1, 0); - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); while let Some(transform) = cursor.item() { if transform.is_isomorphic() && cursor.start().1.column() == 0 { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 328a6a4cc1..709323ef58 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -16,7 +16,7 @@ use crate::{ use collections::VecDeque; use refineable::Refineable as _; use std::{cell::RefCell, ops::Range, rc::Rc}; -use sum_tree::{Bias, SumTree}; +use sum_tree::{Bias, Dimensions, SumTree}; /// Construct a new list element pub fn list(state: ListState) -> List { @@ -371,14 +371,14 @@ impl ListState { return None; } - let mut cursor = state.items.cursor::<(Count, Height)>(&()); + let mut cursor = state.items.cursor::>(&()); cursor.seek(&Count(scroll_top.item_ix), Bias::Right); let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item; cursor.seek_forward(&Count(ix), Bias::Right); if let Some(&ListItem::Measured { size, .. }) = cursor.item() { - let &(Count(count), Height(top)) = cursor.start(); + let &Dimensions(Count(count), Height(top), _) = cursor.start(); if count == ix { let top = bounds.top() + top - scroll_top; return Some(Bounds::from_corners( diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index f441114a90..c56ffed066 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -17,7 +17,7 @@ use std::{ sync::Arc, }; use streaming_iterator::StreamingIterator; -use sum_tree::{Bias, SeekTarget, SumTree}; +use sum_tree::{Bias, Dimensions, SeekTarget, SumTree}; use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint}; use tree_sitter::{Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree}; @@ -285,7 +285,7 @@ impl SyntaxSnapshot { pub fn interpolate(&mut self, text: &BufferSnapshot) { let edits = text - .anchored_edits_since::<(usize, Point)>(&self.interpolated_version) + .anchored_edits_since::>(&self.interpolated_version) .collect::>(); self.interpolated_version = text.version().clone(); @@ -333,7 +333,8 @@ impl SyntaxSnapshot { }; let Some(layer) = cursor.item() else { break }; - let (start_byte, start_point) = layer.range.start.summary::<(usize, Point)>(text); + let Dimensions(start_byte, start_point, _) = + layer.range.start.summary::>(text); // Ignore edits that end before the start of this layer, and don't consider them // for any subsequent layers at this same depth. @@ -562,8 +563,8 @@ impl SyntaxSnapshot { } let Some(step) = step else { break }; - let (step_start_byte, step_start_point) = - step.range.start.summary::<(usize, Point)>(text); + let Dimensions(step_start_byte, step_start_point, _) = + step.range.start.summary::>(text); let step_end_byte = step.range.end.to_offset(text); let mut old_layer = cursor.item(); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f0913e30fb..eb12e6929c 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -43,7 +43,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use sum_tree::{Bias, Cursor, Dimension, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Cursor, Dimension, Dimensions, SumTree, Summary, TreeMap}; use text::{ BufferId, Edit, LineIndent, TextSummary, locator::Locator, @@ -474,7 +474,7 @@ pub struct MultiBufferRows<'a> { pub struct MultiBufferChunks<'a> { excerpts: Cursor<'a, Excerpt, ExcerptOffset>, - diff_transforms: Cursor<'a, DiffTransform, (usize, ExcerptOffset)>, + diff_transforms: Cursor<'a, DiffTransform, Dimensions>, diffs: &'a TreeMap, diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>, buffer_chunk: Option>, @@ -2120,10 +2120,10 @@ impl MultiBuffer { let buffers = self.buffers.borrow(); let mut excerpts = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); + .cursor::, ExcerptDimension>>(&()); let mut diff_transforms = snapshot .diff_transforms - .cursor::<(ExcerptDimension, OutputDimension)>(&()); + .cursor::, OutputDimension>>(&()); diff_transforms.next(); let locators = buffers .get(&buffer_id) @@ -2281,7 +2281,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); let mut edits = Vec::new(); let mut excerpt_ids = ids.iter().copied().peekable(); let mut removed_buffer_ids = Vec::new(); @@ -2492,7 +2492,7 @@ impl MultiBuffer { for locator in &buffer_state.excerpts { let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); cursor.seek_forward(&Some(locator), Bias::Left); if let Some(excerpt) = cursor.item() { if excerpt.locator == *locator { @@ -2845,7 +2845,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); let mut edits = Vec::>::new(); let prefix = cursor.slice(&Some(locator), Bias::Left); @@ -2921,7 +2921,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); let mut edits = Vec::>::new(); for locator in &locators { @@ -3067,7 +3067,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); for (locator, buffer, buffer_edited) in excerpts_to_edit { new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), &()); @@ -3135,7 +3135,7 @@ impl MultiBuffer { let mut excerpts = snapshot.excerpts.cursor::(&()); let mut old_diff_transforms = snapshot .diff_transforms - .cursor::<(ExcerptOffset, usize)>(&()); + .cursor::>(&()); let mut new_diff_transforms = SumTree::default(); let mut old_expanded_hunks = HashSet::default(); let mut output_edits = Vec::new(); @@ -3260,7 +3260,7 @@ impl MultiBuffer { &self, edit: &Edit>, excerpts: &mut Cursor>, - old_diff_transforms: &mut Cursor, usize)>, + old_diff_transforms: &mut Cursor, usize>>, new_diff_transforms: &mut SumTree, end_of_current_insert: &mut Option<(TypedOffset, DiffTransformHunkInfo)>, old_expanded_hunks: &mut HashSet, @@ -4713,7 +4713,9 @@ impl MultiBufferSnapshot { O: ToOffset, { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.diff_transforms.cursor::<(usize, ExcerptOffset)>(&()); + let mut cursor = self + .diff_transforms + .cursor::>(&()); cursor.seek(&range.start, Bias::Right); let Some(first_transform) = cursor.item() else { @@ -4867,7 +4869,10 @@ impl MultiBufferSnapshot { &self, anchor: &Anchor, excerpt_position: D, - diff_transforms: &mut Cursor, OutputDimension)>, + diff_transforms: &mut Cursor< + DiffTransform, + Dimensions, OutputDimension>, + >, ) -> D where D: TextDimension + Ord + Sub, @@ -4927,7 +4932,7 @@ impl MultiBufferSnapshot { fn excerpt_offset_for_anchor(&self, anchor: &Anchor) -> ExcerptOffset { let mut cursor = self .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); let locator = self.excerpt_locator_for_id(anchor.excerpt_id); cursor.seek(&Some(locator), Bias::Left); @@ -4971,7 +4976,7 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::(&()); let mut diff_transforms_cursor = self .diff_transforms - .cursor::<(ExcerptDimension, OutputDimension)>(&()); + .cursor::, OutputDimension>>(&()); diff_transforms_cursor.next(); let mut summaries = Vec::new(); @@ -5201,7 +5206,9 @@ impl MultiBufferSnapshot { // Find the given position in the diff transforms. Determine the corresponding // offset in the excerpts, and whether the position is within a deleted hunk. - let mut diff_transforms = self.diff_transforms.cursor::<(usize, ExcerptOffset)>(&()); + let mut diff_transforms = self + .diff_transforms + .cursor::>(&()); diff_transforms.seek(&offset, Bias::Right); if offset == diff_transforms.start().0 && bias == Bias::Left { @@ -5250,7 +5257,7 @@ impl MultiBufferSnapshot { let mut excerpts = self .excerpts - .cursor::<(ExcerptOffset, Option)>(&()); + .cursor::>>(&()); excerpts.seek(&excerpt_offset, Bias::Right); if excerpts.item().is_none() && excerpt_offset == excerpts.start().0 && bias == Bias::Left { excerpts.prev(); @@ -5341,7 +5348,7 @@ impl MultiBufferSnapshot { let start_locator = self.excerpt_locator_for_id(id); let mut excerpts = self .excerpts - .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); + .cursor::, ExcerptDimension>>(&()); excerpts.seek(&Some(start_locator), Bias::Left); excerpts.prev(); @@ -6242,14 +6249,14 @@ impl MultiBufferSnapshot { pub fn range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { let mut cursor = self .excerpts - .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); + .cursor::, ExcerptDimension>>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); if cursor.seek(&Some(locator), Bias::Left) { let start = cursor.start().1.clone(); let end = cursor.end().1; let mut diff_transforms = self .diff_transforms - .cursor::<(ExcerptDimension, OutputDimension)>(&()); + .cursor::, OutputDimension>>(&()); diff_transforms.seek(&start, Bias::Left); let overshoot = start.0 - diff_transforms.start().0.0; let start = diff_transforms.start().1.0 + overshoot; diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 0329a53cc7..29653748e4 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -6,7 +6,7 @@ use db::smol::stream::StreamExt; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task}; use rpc::{Notification, TypedEnvelope, proto}; use std::{ops::Range, sync::Arc}; -use sum_tree::{Bias, SumTree}; +use sum_tree::{Bias, Dimensions, SumTree}; use time::OffsetDateTime; use util::ResultExt; @@ -360,7 +360,9 @@ impl NotificationStore { is_new: bool, cx: &mut Context, ) { - let mut cursor = self.notifications.cursor::<(NotificationId, Count)>(&()); + let mut cursor = self + .notifications + .cursor::>(&()); let mut new_notifications = SumTree::default(); let mut old_range = 0..0; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 98cecc2e9b..6122204991 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -96,6 +96,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use sum_tree::Dimensions; use text::{Anchor, BufferId, LineEnding, OffsetRangeExt}; use url::Url; use util::{ @@ -7253,7 +7254,9 @@ impl LspStore { let build_incremental_change = || { buffer - .edits_since::<(PointUtf16, usize)>(previous_snapshot.snapshot.version()) + .edits_since::>( + previous_snapshot.snapshot.version(), + ) .map(|edit| { let edit_start = edit.new.start.0; let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 515cd71331..aa3ed5db57 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -12,7 +12,7 @@ use std::{ ops::{self, AddAssign, Range}, str, }; -use sum_tree::{Bias, Dimension, SumTree}; +use sum_tree::{Bias, Dimension, Dimensions, SumTree}; pub use chunk::ChunkSlice; pub use offset_utf16::OffsetUtf16; @@ -282,7 +282,7 @@ impl Rope { if offset >= self.summary().len { return self.summary().len_utf16; } - let mut cursor = self.chunks.cursor::<(usize, OffsetUtf16)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 @@ -295,7 +295,7 @@ impl Rope { if offset >= self.summary().len_utf16 { return self.summary().len; } - let mut cursor = self.chunks.cursor::<(OffsetUtf16, usize)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 @@ -308,7 +308,7 @@ impl Rope { if offset >= self.summary().len { return self.summary().lines; } - let mut cursor = self.chunks.cursor::<(usize, Point)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 @@ -321,7 +321,7 @@ impl Rope { if offset >= self.summary().len { return self.summary().lines_utf16(); } - let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 @@ -334,7 +334,7 @@ impl Rope { if point >= self.summary().lines { return self.summary().lines_utf16(); } - let mut cursor = self.chunks.cursor::<(Point, PointUtf16)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&point, Bias::Left); let overshoot = point - cursor.start().0; cursor.start().1 @@ -347,7 +347,7 @@ impl Rope { if point >= self.summary().lines { return self.summary().len; } - let mut cursor = self.chunks.cursor::<(Point, usize)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&point, Bias::Left); let overshoot = point - cursor.start().0; cursor.start().1 @@ -368,7 +368,7 @@ impl Rope { if point >= self.summary().lines_utf16() { return self.summary().len; } - let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&point, Bias::Left); let overshoot = point - cursor.start().0; cursor.start().1 @@ -381,7 +381,7 @@ impl Rope { if point.0 >= self.summary().lines_utf16() { return self.summary().lines; } - let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&point.0, Bias::Left); let overshoot = Unclipped(point.0 - cursor.start().0); cursor.start().1 @@ -1168,16 +1168,17 @@ pub trait TextDimension: fn add_assign(&mut self, other: &Self); } -impl TextDimension for (D1, D2) { +impl TextDimension for Dimensions { fn from_text_summary(summary: &TextSummary) -> Self { - ( + Dimensions( D1::from_text_summary(summary), D2::from_text_summary(summary), + (), ) } fn from_chunk(chunk: ChunkSlice) -> Self { - (D1::from_chunk(chunk), D2::from_chunk(chunk)) + Dimensions(D1::from_chunk(chunk), D2::from_chunk(chunk), ()) } fn add_assign(&mut self, other: &Self) { diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 4c5ce39590..3a12e3a681 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -101,37 +101,32 @@ impl<'a, T: Summary> Dimension<'a, T> for () { fn add_summary(&mut self, _: &'a T, _: &T::Context) {} } -impl<'a, T: Summary, D1: Dimension<'a, T>, D2: Dimension<'a, T>> Dimension<'a, T> for (D1, D2) { +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct Dimensions(pub D1, pub D2, pub D3); + +impl<'a, T: Summary, D1: Dimension<'a, T>, D2: Dimension<'a, T>, D3: Dimension<'a, T>> + Dimension<'a, T> for Dimensions +{ fn zero(cx: &T::Context) -> Self { - (D1::zero(cx), D2::zero(cx)) + Dimensions(D1::zero(cx), D2::zero(cx), D3::zero(cx)) } fn add_summary(&mut self, summary: &'a T, cx: &T::Context) { self.0.add_summary(summary, cx); self.1.add_summary(summary, cx); + self.2.add_summary(summary, cx); } } -impl<'a, S, D1, D2> SeekTarget<'a, S, (D1, D2)> for D1 -where - S: Summary, - D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, - D2: Dimension<'a, S>, -{ - fn cmp(&self, cursor_location: &(D1, D2), cx: &S::Context) -> Ordering { - self.cmp(&cursor_location.0, cx) - } -} - -impl<'a, S, D1, D2, D3> SeekTarget<'a, S, ((D1, D2), D3)> for D1 +impl<'a, S, D1, D2, D3> SeekTarget<'a, S, Dimensions> for D1 where S: Summary, D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, D2: Dimension<'a, S>, D3: Dimension<'a, S>, { - fn cmp(&self, cursor_location: &((D1, D2), D3), cx: &S::Context) -> Ordering { - self.cmp(&cursor_location.0.0, cx) + fn cmp(&self, cursor_location: &Dimensions, cx: &S::Context) -> Ordering { + self.cmp(&cursor_location.0, cx) } } diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index bf17336f9d..c4778216e0 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -3,7 +3,7 @@ use crate::{ locator::Locator, }; use std::{cmp::Ordering, fmt::Debug, ops::Range}; -use sum_tree::Bias; +use sum_tree::{Bias, Dimensions}; /// A timestamped position in a buffer #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)] @@ -102,7 +102,9 @@ impl Anchor { let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else { return false; }; - let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut fragment_cursor = buffer + .fragments + .cursor::, usize>>(&None); fragment_cursor.seek(&Some(fragment_id), Bias::Left); fragment_cursor .item() diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index aded03d46a..68c7b2a2cd 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -37,7 +37,7 @@ use std::{ }; pub use subscription::*; pub use sum_tree::Bias; -use sum_tree::{FilterCursor, SumTree, TreeMap, TreeSet}; +use sum_tree::{Dimensions, FilterCursor, SumTree, TreeMap, TreeSet}; use undo_map::UndoMap; #[cfg(any(test, feature = "test-support"))] @@ -1071,7 +1071,9 @@ impl Buffer { let mut insertion_offset = 0; let mut new_ropes = RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - let mut old_fragments = self.fragments.cursor::<(VersionedFullOffset, usize)>(&cx); + let mut old_fragments = self + .fragments + .cursor::>(&cx); let mut new_fragments = old_fragments.slice(&VersionedFullOffset::Offset(ranges[0].start), Bias::Left); new_ropes.append(new_fragments.summary().text); @@ -1298,7 +1300,9 @@ impl Buffer { self.snapshot.undo_map.insert(undo); let mut edits = Patch::default(); - let mut old_fragments = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut old_fragments = self + .fragments + .cursor::, usize>>(&None); let mut new_fragments = SumTree::new(&None); let mut new_ropes = RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); @@ -1561,7 +1565,9 @@ impl Buffer { D: TextDimension, { // get fragment ranges - let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut cursor = self + .fragments + .cursor::, usize>>(&None); let offset_ranges = self .fragment_ids_for_edits(edit_ids.into_iter()) .into_iter() @@ -2232,7 +2238,9 @@ impl BufferSnapshot { { let anchors = anchors.into_iter(); let mut insertion_cursor = self.insertions.cursor::(&()); - let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut fragment_cursor = self + .fragments + .cursor::, usize>>(&None); let mut text_cursor = self.visible_text.cursor(0); let mut position = D::zero(&()); @@ -2318,7 +2326,9 @@ impl BufferSnapshot { ); }; - let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut fragment_cursor = self + .fragments + .cursor::, usize>>(&None); fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left); let fragment = fragment_cursor.item().unwrap(); let mut fragment_offset = fragment_cursor.start().1; @@ -2476,7 +2486,7 @@ impl BufferSnapshot { }; let mut cursor = self .fragments - .cursor::<(Option<&Locator>, FragmentTextSummary)>(&None); + .cursor::, FragmentTextSummary>>(&None); let start_fragment_id = self.fragment_id_for_anchor(&range.start); cursor.seek(&Some(start_fragment_id), Bias::Left); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index e6949f62df..b5a0f71e81 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -62,7 +62,7 @@ use std::{ }, time::{Duration, Instant}, }; -use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; +use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ ResultExt, @@ -3566,10 +3566,15 @@ impl<'a> sum_tree::Dimension<'a, PathSummary> for GitSummary { } } -impl<'a> sum_tree::SeekTarget<'a, PathSummary, (TraversalProgress<'a>, GitSummary)> +impl<'a> + sum_tree::SeekTarget<'a, PathSummary, Dimensions, GitSummary>> for PathTarget<'_> { - fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitSummary), _: &()) -> Ordering { + fn cmp( + &self, + cursor_location: &Dimensions, GitSummary>, + _: &(), + ) -> Ordering { self.cmp_path(&cursor_location.0.max_path) } } From 669c57b45ff7a214b6a3efce8835bb1e4d137489 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Mon, 4 Aug 2025 20:19:42 -0500 Subject: [PATCH 31/46] Add minidump crash reporting (#35263) - [x] Handle uploading minidumps from the remote_server - [x] Associate minidumps with panics with some sort of ID (we don't use session_id on the remote) - [x] Update the protobufs and client/server code to request panics - [x] Upload minidumps with no corresponding panic - [x] Fill in panic info when there _is_ a corresponding panic - [x] Use an env var for the sentry endpoint instead of hardcoding it Release Notes: - Zed now generates minidumps for crash reporting --------- Co-authored-by: Max Brunsfeld --- .github/workflows/ci.yml | 1 + .github/workflows/nix.yml | 1 + .github/workflows/release_nightly.yml | 1 + Cargo.lock | 189 +++++++++++++++++++- Cargo.toml | 5 + crates/client/src/telemetry.rs | 6 + crates/crashes/Cargo.toml | 20 +++ crates/crashes/LICENSE-GPL | 1 + crates/crashes/src/crashes.rs | 172 ++++++++++++++++++ crates/http_client/Cargo.toml | 1 + crates/http_client/src/async_body.rs | 11 ++ crates/http_client/src/http_client.rs | 77 +++----- crates/proto/proto/app.proto | 11 +- crates/proto/proto/zed.proto | 9 +- crates/proto/src/proto.rs | 6 +- crates/remote_server/Cargo.toml | 3 + crates/remote_server/src/main.rs | 9 + crates/remote_server/src/unix.rs | 108 +++++++---- crates/reqwest_client/src/reqwest_client.rs | 23 ++- crates/zed/Cargo.toml | 2 + crates/zed/src/main.rs | 14 ++ crates/zed/src/reliability.rs | 139 +++++++++++--- docs/src/development/debugging-crashes.md | 1 + docs/src/telemetry.md | 6 +- tooling/workspace-hack/Cargo.toml | 28 +-- 25 files changed, 709 insertions(+), 135 deletions(-) create mode 100644 crates/crashes/Cargo.toml create mode 120000 crates/crashes/LICENSE-GPL create mode 100644 crates/crashes/src/crashes.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dfc33e0d2..c08f4ac211 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ env: DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} + ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} jobs: job_spec: diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index beacd27774..c019f805fe 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -29,6 +29,7 @@ jobs: runs-on: ${{ matrix.system.runner }} env: ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} + ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }} GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on steps: diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 4f7506967b..69e5f86cb6 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -13,6 +13,7 @@ env: CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} + ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} diff --git a/Cargo.lock b/Cargo.lock index 6239c83fdc..eae04776d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1172,7 +1172,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_qs 0.10.1", - "smart-default", + "smart-default 0.6.0", "smol_str 0.1.24", "thiserror 1.0.69", "tokio", @@ -3927,6 +3927,42 @@ dependencies = [ "target-lexicon 0.13.2", ] +[[package]] +name = "crash-context" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3" +dependencies = [ + "cfg-if", + "libc", + "mach2", +] + +[[package]] +name = "crash-handler" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2066907075af649bcb8bcb1b9b986329b243677e6918b2d920aa64b0aac5ace3" +dependencies = [ + "cfg-if", + "crash-context", + "libc", + "mach2", + "parking_lot", +] + +[[package]] +name = "crashes" +version = "0.1.0" +dependencies = [ + "crash-handler", + "log", + "minidumper", + "paths", + "smol", + "workspace-hack", +] + [[package]] name = "crc" version = "3.2.1" @@ -4453,6 +4489,15 @@ dependencies = [ "zlog", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "deepseek" version = "0.1.0" @@ -7235,6 +7280,17 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "google_ai" version = "0.1.0" @@ -7850,6 +7906,7 @@ dependencies = [ "http-body 1.0.1", "log", "parking_lot", + "reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)", "serde", "serde_json", "url", @@ -10080,6 +10137,63 @@ dependencies = [ "unicase", ] +[[package]] +name = "minidump-common" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77" +dependencies = [ + "bitflags 2.9.0", + "debugid", + "num-derive", + "num-traits", + "range-map", + "scroll", + "smart-default 0.7.1", +] + +[[package]] +name = "minidump-writer" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d" +dependencies = [ + "bitflags 2.9.0", + "byteorder", + "cfg-if", + "crash-context", + "goblin", + "libc", + "log", + "mach2", + "memmap2", + "memoffset", + "minidump-common", + "nix 0.28.0", + "procfs-core", + "scroll", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "minidumper" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4ebc9d1f8847ec1d078f78b35ed598e0ebefa1f242d5f83cd8d7f03960a7d1" +dependencies = [ + "cfg-if", + "crash-context", + "libc", + "log", + "minidump-writer", + "parking_lot", + "polling", + "scroll", + "thiserror 1.0.69", + "uds", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -12069,6 +12183,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.7.1" @@ -12329,6 +12449,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags 2.9.0", + "hex", +] + [[package]] name = "prodash" version = "29.0.2" @@ -12979,6 +13109,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "range-map" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f" +dependencies = [ + "num-traits", +] + [[package]] name = "rangemap" version = "1.5.1" @@ -13321,6 +13460,8 @@ dependencies = [ "clap", "client", "clock", + "crash-handler", + "crashes", "dap", "dap_adapters", "debug_adapter_extension", @@ -13344,6 +13485,7 @@ dependencies = [ "libc", "log", "lsp", + "minidumper", "node_runtime", "paths", "project", @@ -13532,6 +13674,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", @@ -14260,6 +14403,26 @@ dependencies = [ "once_cell", ] +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -15005,6 +15168,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "smart-default" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "smol" version = "2.0.2" @@ -17288,6 +17462,15 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uds" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661" +dependencies = [ + "libc", +] + [[package]] name = "uds_windows" version = "1.1.0" @@ -19743,9 +19926,11 @@ dependencies = [ "lyon_path", "md-5", "memchr", + "mime_guess", "miniz_oxide", "mio 1.0.3", "naga", + "nix 0.28.0", "nix 0.29.0", "nom", "num-bigint", @@ -20217,6 +20402,7 @@ dependencies = [ "command_palette", "component", "copilot", + "crashes", "dap", "dap_adapters", "db", @@ -20284,6 +20470,7 @@ dependencies = [ "release_channel", "remote", "repl", + "reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)", "reqwest_client", "rope", "search", diff --git a/Cargo.toml b/Cargo.toml index d5982116f3..733db92ce9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "crates/component", "crates/context_server", "crates/copilot", + "crates/crashes", "crates/credentials_provider", "crates/dap", "crates/dap_adapters", @@ -266,6 +267,7 @@ command_palette_hooks = { path = "crates/command_palette_hooks" } component = { path = "crates/component" } context_server = { path = "crates/context_server" } copilot = { path = "crates/copilot" } +crashes = { path = "crates/crashes" } credentials_provider = { path = "crates/credentials_provider" } dap = { path = "crates/dap" } dap_adapters = { path = "crates/dap_adapters" } @@ -466,6 +468,7 @@ core-foundation = "0.10.0" core-foundation-sys = "0.8.6" core-video = { version = "0.4.3", features = ["metal"] } cpal = "0.16" +crash-handler = "0.6" criterion = { version = "0.5", features = ["html_reports"] } ctor = "0.4.0" dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" } @@ -513,6 +516,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" } markup5ever_rcdom = "0.3.0" metal = "0.29" +minidumper = "0.8" moka = { version = "0.12.10", features = ["sync"] } naga = { version = "25.0", features = ["wgsl-in"] } nanoid = "0.4" @@ -552,6 +556,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 "charset", "http2", "macos-system-configuration", + "multipart", "rustls-tls-native-roots", "socks", "stream", diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 7d39464e4a..4a8e745fcb 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -74,6 +74,12 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock>> = LazyLock::new(|| { }) }); +pub static SENTRY_MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { + option_env!("SENTRY_MINIDUMP_ENDPOINT") + .map(|s| s.to_owned()) + .or_else(|| env::var("SENTRY_MINIDUMP_ENDPOINT").ok()) +}); + static DOTNET_PROJECT_FILES_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap() }); diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml new file mode 100644 index 0000000000..641a97765a --- /dev/null +++ b/crates/crashes/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "crashes" +version = "0.1.0" +publish.workspace = true +edition.workspace = true +license = "GPL-3.0-or-later" + +[dependencies] +crash-handler.workspace = true +log.workspace = true +minidumper.workspace = true +paths.workspace = true +smol.workspace = true +workspace-hack.workspace = true + +[lints] +workspace = true + +[lib] +path = "src/crashes.rs" diff --git a/crates/crashes/LICENSE-GPL b/crates/crashes/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/crashes/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs new file mode 100644 index 0000000000..cfb4b57d5d --- /dev/null +++ b/crates/crashes/src/crashes.rs @@ -0,0 +1,172 @@ +use crash_handler::CrashHandler; +use log::info; +use minidumper::{Client, LoopAction, MinidumpBinary}; + +use std::{ + env, + fs::File, + io, + path::{Path, PathBuf}, + process::{self, Command}, + sync::{ + OnceLock, + atomic::{AtomicBool, Ordering}, + }, + thread, + time::Duration, +}; + +// set once the crash handler has initialized and the client has connected to it +pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false); +// set when the first minidump request is made to avoid generating duplicate crash reports +pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false); +const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60); + +pub async fn init(id: String) { + let exe = env::current_exe().expect("unable to find ourselves"); + let zed_pid = process::id(); + // TODO: we should be able to get away with using 1 crash-handler process per machine, + // but for now we append the PID of the current process which makes it unique per remote + // server or interactive zed instance. This solves an issue where occasionally the socket + // used by the crash handler isn't destroyed correctly which causes it to stay on the file + // system and block further attempts to initialize crash handlers with that socket path. + let socket_name = paths::temp_dir().join(format!("zed-crash-handler-{zed_pid}")); + #[allow(unused)] + let server_pid = Command::new(exe) + .arg("--crash-handler") + .arg(&socket_name) + .spawn() + .expect("unable to spawn server process") + .id(); + info!("spawning crash handler process"); + + let mut elapsed = Duration::ZERO; + let retry_frequency = Duration::from_millis(100); + let mut maybe_client = None; + while maybe_client.is_none() { + if let Ok(client) = Client::with_name(socket_name.as_path()) { + maybe_client = Some(client); + info!("connected to crash handler process after {elapsed:?}"); + break; + } + elapsed += retry_frequency; + smol::Timer::after(retry_frequency).await; + } + let client = maybe_client.unwrap(); + client.send_message(1, id).unwrap(); // set session id on the server + + let client = std::sync::Arc::new(client); + let handler = crash_handler::CrashHandler::attach(unsafe { + let client = client.clone(); + crash_handler::make_crash_event(move |crash_context: &crash_handler::CrashContext| { + // only request a minidump once + let res = if REQUESTED_MINIDUMP + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + client.send_message(2, "mistakes were made").unwrap(); + client.ping().unwrap(); + client.request_dump(crash_context).is_ok() + } else { + true + }; + crash_handler::CrashEventResult::Handled(res) + }) + }) + .expect("failed to attach signal handler"); + + #[cfg(target_os = "linux")] + { + handler.set_ptracer(Some(server_pid)); + } + CRASH_HANDLER.store(true, Ordering::Release); + std::mem::forget(handler); + info!("crash handler registered"); + + loop { + client.ping().ok(); + smol::Timer::after(Duration::from_secs(10)).await; + } +} + +pub struct CrashServer { + session_id: OnceLock, +} + +impl minidumper::ServerHandler for CrashServer { + fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> { + let err_message = "Need to send a message with the ID upon starting the crash handler"; + let dump_path = paths::logs_dir() + .join(self.session_id.get().expect(err_message)) + .with_extension("dmp"); + let file = File::create(&dump_path)?; + Ok((file, dump_path)) + } + + fn on_minidump_created(&self, result: Result) -> LoopAction { + match result { + Ok(mut md_bin) => { + use io::Write; + let _ = md_bin.file.flush(); + info!("wrote minidump to disk {:?}", md_bin.path); + } + Err(e) => { + info!("failed to write minidump: {:#}", e); + } + } + LoopAction::Exit + } + + fn on_message(&self, kind: u32, buffer: Vec) { + let message = String::from_utf8(buffer).expect("invalid utf-8"); + info!("kind: {kind}, message: {message}",); + if kind == 1 { + self.session_id + .set(message) + .expect("session id already initialized"); + } + } + + fn on_client_disconnected(&self, clients: usize) -> LoopAction { + info!("client disconnected, {clients} remaining"); + if clients == 0 { + LoopAction::Exit + } else { + LoopAction::Continue + } + } +} + +pub fn handle_panic() { + // wait 500ms for the crash handler process to start up + // if it's still not there just write panic info and no minidump + let retry_frequency = Duration::from_millis(100); + for _ in 0..5 { + if CRASH_HANDLER.load(Ordering::Acquire) { + log::error!("triggering a crash to generate a minidump..."); + #[cfg(target_os = "linux")] + CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32); + #[cfg(not(target_os = "linux"))] + CrashHandler.simulate_exception(None); + break; + } + thread::sleep(retry_frequency); + } +} + +pub fn crash_server(socket: &Path) { + let Ok(mut server) = minidumper::Server::with_name(socket) else { + log::info!("Couldn't create socket, there may already be a running crash server"); + return; + }; + let ab = AtomicBool::new(false); + server + .run( + Box::new(CrashServer { + session_id: OnceLock::new(), + }), + &ab, + Some(CRASH_HANDLER_TIMEOUT), + ) + .expect("failed to run server"); +} diff --git a/crates/http_client/Cargo.toml b/crates/http_client/Cargo.toml index 3f51cc5a23..f63bff295e 100644 --- a/crates/http_client/Cargo.toml +++ b/crates/http_client/Cargo.toml @@ -24,6 +24,7 @@ http.workspace = true http-body.workspace = true log.workspace = true parking_lot.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true url.workspace = true diff --git a/crates/http_client/src/async_body.rs b/crates/http_client/src/async_body.rs index 88972d279c..473849f3cd 100644 --- a/crates/http_client/src/async_body.rs +++ b/crates/http_client/src/async_body.rs @@ -88,6 +88,17 @@ impl From<&'static str> for AsyncBody { } } +impl TryFrom for AsyncBody { + type Error = anyhow::Error; + + fn try_from(value: reqwest::Body) -> Result { + value + .as_bytes() + .ok_or_else(|| anyhow::anyhow!("Underlying data is a stream")) + .map(|bytes| Self::from_bytes(Bytes::copy_from_slice(bytes))) + } +} + impl> From> for AsyncBody { fn from(body: Option) -> Self { match body { diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index d33bbefc06..a7f75b0962 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -7,7 +7,10 @@ use derive_more::Deref; use http::HeaderValue; pub use http::{self, Method, Request, Response, StatusCode, Uri}; -use futures::future::BoxFuture; +use futures::{ + FutureExt as _, + future::{self, BoxFuture}, +}; use http::request::Builder; use parking_lot::Mutex; #[cfg(feature = "test-support")] @@ -89,6 +92,14 @@ pub trait HttpClient: 'static + Send + Sync { fn as_fake(&self) -> &FakeHttpClient { panic!("called as_fake on {}", type_name::()) } + + fn send_multipart_form<'a>( + &'a self, + _url: &str, + _request: reqwest::multipart::Form, + ) -> BoxFuture<'a, anyhow::Result>> { + future::ready(Err(anyhow!("not implemented"))).boxed() + } } /// An [`HttpClient`] that may have a proxy. @@ -140,31 +151,13 @@ impl HttpClient for HttpClientWithProxy { fn as_fake(&self) -> &FakeHttpClient { self.client.as_fake() } -} -impl HttpClient for Arc { - fn send( - &self, - req: Request, - ) -> BoxFuture<'static, anyhow::Result>> { - self.client.send(req) - } - - fn user_agent(&self) -> Option<&HeaderValue> { - self.client.user_agent() - } - - fn proxy(&self) -> Option<&Url> { - self.proxy.as_ref() - } - - fn type_name(&self) -> &'static str { - self.client.type_name() - } - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - self.client.as_fake() + fn send_multipart_form<'a>( + &'a self, + url: &str, + form: reqwest::multipart::Form, + ) -> BoxFuture<'a, anyhow::Result>> { + self.client.send_multipart_form(url, form) } } @@ -275,32 +268,6 @@ impl HttpClientWithUrl { } } -impl HttpClient for Arc { - fn send( - &self, - req: Request, - ) -> BoxFuture<'static, anyhow::Result>> { - self.client.send(req) - } - - fn user_agent(&self) -> Option<&HeaderValue> { - self.client.user_agent() - } - - fn proxy(&self) -> Option<&Url> { - self.client.proxy.as_ref() - } - - fn type_name(&self) -> &'static str { - self.client.type_name() - } - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - self.client.as_fake() - } -} - impl HttpClient for HttpClientWithUrl { fn send( &self, @@ -325,6 +292,14 @@ impl HttpClient for HttpClientWithUrl { fn as_fake(&self) -> &FakeHttpClient { self.client.as_fake() } + + fn send_multipart_form<'a>( + &'a self, + url: &str, + request: reqwest::multipart::Form, + ) -> BoxFuture<'a, anyhow::Result>> { + self.client.send_multipart_form(url, request) + } } pub fn read_proxy_from_env() -> Option { diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 5330ee506a..353f19adb2 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -79,11 +79,16 @@ message OpenServerSettings { uint64 project_id = 1; } -message GetPanicFiles { +message GetCrashFiles { } -message GetPanicFilesResponse { - repeated string file_contents = 2; +message GetCrashFilesResponse { + repeated CrashReport crashes = 1; +} + +message CrashReport { + optional string panic_contents = 1; + optional bytes minidump_contents = 2; } message Extension { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index d511ea5e8f..9de5c2c0c7 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -294,9 +294,6 @@ message Envelope { GetPathMetadata get_path_metadata = 278; GetPathMetadataResponse get_path_metadata_response = 279; - GetPanicFiles get_panic_files = 280; - GetPanicFilesResponse get_panic_files_response = 281; - CancelLanguageServerWork cancel_language_server_work = 282; LspExtOpenDocs lsp_ext_open_docs = 283; @@ -402,7 +399,10 @@ message Envelope { StashPop stash_pop = 358; GetDefaultBranch get_default_branch = 359; - GetDefaultBranchResponse get_default_branch_response = 360; // current max + GetDefaultBranchResponse get_default_branch_response = 360; + + GetCrashFiles get_crash_files = 361; + GetCrashFilesResponse get_crash_files_response = 362; // current max } reserved 87 to 88; @@ -423,6 +423,7 @@ message Envelope { reserved 270; reserved 247 to 254; reserved 255 to 256; + reserved 280 to 281; } message Hello { diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 72b3807deb..4c447e2eca 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -99,8 +99,8 @@ messages!( (GetHoverResponse, Background), (GetNotifications, Foreground), (GetNotificationsResponse, Foreground), - (GetPanicFiles, Background), - (GetPanicFilesResponse, Background), + (GetCrashFiles, Background), + (GetCrashFilesResponse, Background), (GetPathMetadata, Background), (GetPathMetadataResponse, Background), (GetPermalinkToLine, Foreground), @@ -462,7 +462,7 @@ request_messages!( (ActivateToolchain, Ack), (ActiveToolchain, ActiveToolchainResponse), (GetPathMetadata, GetPathMetadataResponse), - (GetPanicFiles, GetPanicFilesResponse), + (GetCrashFiles, GetCrashFilesResponse), (CancelLanguageServerWork, Ack), (SyncExtensions, SyncExtensionsResponse), (InstallExtension, Ack), diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 443c47919f..c6a546f345 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -67,8 +67,11 @@ watch.workspace = true worktree.workspace = true [target.'cfg(not(windows))'.dependencies] +crashes.workspace = true +crash-handler.workspace = true fork.workspace = true libc.workspace = true +minidumper.workspace = true [dev-dependencies] assistant_tool.workspace = true diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 98f635d856..03b0c3eda3 100644 --- a/crates/remote_server/src/main.rs +++ b/crates/remote_server/src/main.rs @@ -12,6 +12,10 @@ struct Cli { /// by having Zed act like netcat communicating over a Unix socket. #[arg(long, hide = true)] askpass: Option, + /// Used for recording minidumps on crashes by having the server run a separate + /// process communicating over a socket. + #[arg(long, hide = true)] + crash_handler: Option, /// Used for loading the environment from the project. #[arg(long, hide = true)] printenv: bool, @@ -58,6 +62,11 @@ fn main() { return; } + if let Some(socket) = &cli.crash_handler { + crashes::crash_server(socket.as_path()); + return; + } + if cli.printenv { util::shell_env::print_env(); return; diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 84ce08ff25..9bb5645dc7 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -17,6 +17,7 @@ use node_runtime::{NodeBinaryOptions, NodeRuntime}; use paths::logs_dir; use project::project_settings::ProjectSettings; +use proto::CrashReport; use release_channel::{AppVersion, RELEASE_CHANNEL, ReleaseChannel}; use remote::proxy::ProxyLaunchError; use remote::ssh_session::ChannelClient; @@ -33,6 +34,7 @@ use smol::io::AsyncReadExt; use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; +use std::collections::HashMap; use std::ffi::OsStr; use std::ops::ControlFlow; use std::str::FromStr; @@ -109,8 +111,9 @@ fn init_logging_server(log_file_path: PathBuf) -> Result>> { Ok(rx) } -fn init_panic_hook() { - std::panic::set_hook(Box::new(|info| { +fn init_panic_hook(session_id: String) { + std::panic::set_hook(Box::new(move |info| { + crashes::handle_panic(); let payload = info .payload() .downcast_ref::<&str>() @@ -171,9 +174,11 @@ fn init_panic_hook() { architecture: env::consts::ARCH.into(), panicked_on: Utc::now().timestamp_millis(), backtrace, - system_id: None, // Set on SSH client - installation_id: None, // Set on SSH client - session_id: "".to_string(), // Set on SSH client + system_id: None, // Set on SSH client + installation_id: None, // Set on SSH client + + // used on this end to associate panics with minidumps, but will be replaced on the SSH client + session_id: session_id.clone(), }; if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { @@ -194,44 +199,69 @@ fn init_panic_hook() { })); } -fn handle_panic_requests(project: &Entity, client: &Arc) { +fn handle_crash_files_requests(project: &Entity, client: &Arc) { let client: AnyProtoClient = client.clone().into(); client.add_request_handler( project.downgrade(), - |_, _: TypedEnvelope, _cx| async move { + |_, _: TypedEnvelope, _cx| async move { + let mut crashes = Vec::new(); + let mut minidumps_by_session_id = HashMap::new(); let mut children = smol::fs::read_dir(paths::logs_dir()).await?; - let mut panic_files = Vec::new(); while let Some(child) = children.next().await { let child = child?; let child_path = child.path(); - if child_path.extension() != Some(OsStr::new("panic")) { - continue; + let extension = child_path.extension(); + if extension == Some(OsStr::new("panic")) { + let filename = if let Some(filename) = child_path.file_name() { + filename.to_string_lossy() + } else { + continue; + }; + + if !filename.starts_with("zed") { + continue; + } + + let file_contents = smol::fs::read_to_string(&child_path) + .await + .context("error reading panic file")?; + + crashes.push(proto::CrashReport { + panic_contents: Some(file_contents), + minidump_contents: None, + }); + } else if extension == Some(OsStr::new("dmp")) { + let session_id = child_path.file_stem().unwrap().to_string_lossy(); + minidumps_by_session_id + .insert(session_id.to_string(), smol::fs::read(&child_path).await?); } - let filename = if let Some(filename) = child_path.file_name() { - filename.to_string_lossy() - } else { - continue; - }; - - if !filename.starts_with("zed") { - continue; - } - - let file_contents = smol::fs::read_to_string(&child_path) - .await - .context("error reading panic file")?; - - panic_files.push(file_contents); // We've done what we can, delete the file - std::fs::remove_file(child_path) + smol::fs::remove_file(&child_path) + .await .context("error removing panic") .log_err(); } - anyhow::Ok(proto::GetPanicFilesResponse { - file_contents: panic_files, - }) + + for crash in &mut crashes { + let panic: telemetry_events::Panic = + serde_json::from_str(crash.panic_contents.as_ref().unwrap())?; + if let dump @ Some(_) = minidumps_by_session_id.remove(&panic.session_id) { + crash.minidump_contents = dump; + } + } + + crashes.extend( + minidumps_by_session_id + .into_values() + .map(|dmp| CrashReport { + panic_contents: None, + minidump_contents: Some(dmp), + }), + ); + + anyhow::Ok(proto::GetCrashFilesResponse { crashes }) }, ); } @@ -409,7 +439,12 @@ pub fn execute_run( ControlFlow::Continue(_) => {} } - init_panic_hook(); + let app = gpui::Application::headless(); + let id = std::process::id().to_string(); + app.background_executor() + .spawn(crashes::init(id.clone())) + .detach(); + init_panic_hook(id); let log_rx = init_logging_server(log_file)?; log::info!( "starting up. pid_file: {:?}, stdin_socket: {:?}, stdout_socket: {:?}, stderr_socket: {:?}", @@ -425,7 +460,7 @@ pub fn execute_run( let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?; let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); - gpui::Application::headless().run(move |cx| { + app.run(move |cx| { settings::init(cx); let app_version = AppVersion::load(env!("ZED_PKG_VERSION")); release_channel::init(app_version, cx); @@ -486,7 +521,7 @@ pub fn execute_run( ) }); - handle_panic_requests(&project, &session); + handle_crash_files_requests(&project, &session); cx.background_spawn(async move { cleanup_old_binaries() }) .detach(); @@ -530,12 +565,15 @@ impl ServerPaths { pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { init_logging_proxy(); - init_panic_hook(); - - log::info!("starting proxy process. PID: {}", std::process::id()); let server_paths = ServerPaths::new(&identifier)?; + let id = std::process::id().to_string(); + smol::spawn(crashes::init(id.clone())).detach(); + init_panic_hook(id); + + log::info!("starting proxy process. PID: {}", std::process::id()); + let server_pid = check_pid_file(&server_paths.pid_file)?; let server_running = server_pid.is_some(); if is_reconnecting { diff --git a/crates/reqwest_client/src/reqwest_client.rs b/crates/reqwest_client/src/reqwest_client.rs index e02768876d..6461a0ae17 100644 --- a/crates/reqwest_client/src/reqwest_client.rs +++ b/crates/reqwest_client/src/reqwest_client.rs @@ -4,14 +4,13 @@ use std::{any::type_name, borrow::Cow, mem, pin::Pin, task::Poll, time::Duration use anyhow::anyhow; use bytes::{BufMut, Bytes, BytesMut}; -use futures::{AsyncRead, TryStreamExt as _}; +use futures::{AsyncRead, FutureExt as _, TryStreamExt as _}; use http_client::{RedirectPolicy, Url, http}; use regex::Regex; use reqwest::{ header::{HeaderMap, HeaderValue}, redirect, }; -use smol::future::FutureExt; const DEFAULT_CAPACITY: usize = 4096; static RUNTIME: OnceLock = OnceLock::new(); @@ -274,6 +273,26 @@ impl http_client::HttpClient for ReqwestClient { } .boxed() } + + fn send_multipart_form<'a>( + &'a self, + url: &str, + form: reqwest::multipart::Form, + ) -> futures::future::BoxFuture<'a, anyhow::Result>> + { + let response = self.client.post(url).multipart(form).send(); + self.handle + .spawn(async move { + let response = response.await?; + let mut builder = http::response::Builder::new().status(response.status()); + for (k, v) in response.headers() { + builder = builder.header(k, v) + } + Ok(builder.body(response.bytes().await?.into())?) + }) + .map(|e| e?) + .boxed() + } } #[cfg(test)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bdd8db9027..5bd6d981fa 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -45,6 +45,7 @@ collections.workspace = true command_palette.workspace = true component.workspace = true copilot.workspace = true +crashes.workspace = true dap_adapters.workspace = true db.workspace = true debug_adapter_extension.workspace = true @@ -117,6 +118,7 @@ recent_projects.workspace = true release_channel.workspace = true remote.workspace = true repl.workspace = true +reqwest.workspace = true reqwest_client.workspace = true rope.workspace = true search.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 71b29909a1..e4a14b5d32 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -172,6 +172,12 @@ pub fn main() { let args = Args::parse(); + // `zed --crash-handler` Makes zed operate in minidump crash handler mode + if let Some(socket) = &args.crash_handler { + crashes::crash_server(socket.as_path()); + return; + } + // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass if let Some(socket) = &args.askpass { askpass::main(socket); @@ -264,6 +270,9 @@ pub fn main() { let session_id = Uuid::new_v4().to_string(); let session = app.background_executor().block(Session::new()); + app.background_executor() + .spawn(crashes::init(session_id.clone())) + .detach(); reliability::init_panic_hook( app_version, app_commit_sha.clone(), @@ -1185,6 +1194,11 @@ struct Args { #[arg(long, hide = true)] nc: Option, + /// Used for recording minidumps on crashes by having Zed run a separate + /// process communicating over a socket. + #[arg(long, hide = true)] + crash_handler: Option, + /// Run zed in the foreground, only used on Windows, to match the behavior on macOS. #[arg(long)] #[cfg(target_os = "windows")] diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index d7f1473288..9157f66216 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -2,21 +2,32 @@ use crate::stdout_is_a_pty; use anyhow::{Context as _, Result}; use backtrace::{self, Backtrace}; use chrono::Utc; -use client::{TelemetrySettings, telemetry}; +use client::{ + TelemetrySettings, + telemetry::{self, SENTRY_MINIDUMP_ENDPOINT}, +}; use db::kvp::KEY_VALUE_STORE; +use futures::AsyncReadExt; use gpui::{App, AppContext as _, SemanticVersion}; use http_client::{self, HttpClient, HttpClientWithUrl, HttpRequestExt, Method}; use paths::{crashes_dir, crashes_retired_dir}; use project::Project; use release_channel::{AppCommitSha, RELEASE_CHANNEL, ReleaseChannel}; +use reqwest::multipart::{Form, Part}; use settings::Settings; use smol::stream::StreamExt; use std::{ env, ffi::{OsStr, c_void}, - sync::{Arc, atomic::Ordering}, + fs, + io::Write, + panic, + sync::{ + Arc, + atomic::{AtomicU32, Ordering}, + }, + thread, }; -use std::{io::Write, panic, sync::atomic::AtomicU32, thread}; use telemetry_events::{LocationData, Panic, PanicRequest}; use url::Url; use util::ResultExt; @@ -37,9 +48,10 @@ pub fn init_panic_hook( if prior_panic_count > 0 { // Give the panic-ing thread time to write the panic file loop { - std::thread::yield_now(); + thread::yield_now(); } } + crashes::handle_panic(); let thread = thread::current(); let thread_name = thread.name().unwrap_or(""); @@ -136,9 +148,8 @@ pub fn init_panic_hook( if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic")); - let panic_file = std::fs::OpenOptions::new() - .append(true) - .create(true) + let panic_file = fs::OpenOptions::new() + .create_new(true) .open(&panic_file_path) .log_err(); if let Some(mut panic_file) = panic_file { @@ -205,27 +216,31 @@ pub fn init( if let Some(ssh_client) = project.ssh_client() { ssh_client.update(cx, |client, cx| { if TelemetrySettings::get_global(cx).diagnostics { - let request = client.proto_client().request(proto::GetPanicFiles {}); + let request = client.proto_client().request(proto::GetCrashFiles {}); cx.background_spawn(async move { - let panic_files = request.await?; - for file in panic_files.file_contents { - let panic: Option = serde_json::from_str(&file) - .log_err() - .or_else(|| { - file.lines() - .next() - .and_then(|line| serde_json::from_str(line).ok()) - }) - .unwrap_or_else(|| { - log::error!("failed to deserialize panic file {:?}", file); - None - }); + let crash_files = request.await?; + for crash in crash_files.crashes { + let mut panic: Option = crash + .panic_contents + .and_then(|s| serde_json::from_str(&s).log_err()); - if let Some(mut panic) = panic { + if let Some(panic) = panic.as_mut() { panic.session_id = session_id.clone(); panic.system_id = system_id.clone(); panic.installation_id = installation_id.clone(); + } + if let Some(minidump) = crash.minidump_contents { + upload_minidump( + http_client.clone(), + minidump.clone(), + panic.as_ref(), + ) + .await + .log_err(); + } + + if let Some(panic) = panic { upload_panic(&http_client, &panic_report_url, panic, &mut None) .await?; } @@ -510,6 +525,22 @@ async fn upload_previous_panics( }); if let Some(panic) = panic { + let minidump_path = paths::logs_dir() + .join(&panic.session_id) + .with_extension("dmp"); + if minidump_path.exists() { + let minidump = smol::fs::read(&minidump_path) + .await + .context("Failed to read minidump")?; + if upload_minidump(http.clone(), minidump, Some(&panic)) + .await + .log_err() + .is_some() + { + fs::remove_file(minidump_path).ok(); + } + } + if !upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? { continue; } @@ -517,13 +548,75 @@ async fn upload_previous_panics( } // We've done what we can, delete the file - std::fs::remove_file(child_path) + fs::remove_file(child_path) .context("error removing panic") .log_err(); } + + // loop back over the directory again to upload any minidumps that are missing panics + let mut children = smol::fs::read_dir(paths::logs_dir()).await?; + while let Some(child) = children.next().await { + let child = child?; + let child_path = child.path(); + if child_path.extension() != Some(OsStr::new("dmp")) { + continue; + } + if upload_minidump( + http.clone(), + smol::fs::read(&child_path) + .await + .context("Failed to read minidump")?, + None, + ) + .await + .log_err() + .is_some() + { + fs::remove_file(child_path).ok(); + } + } + Ok(most_recent_panic) } +async fn upload_minidump( + http: Arc, + minidump: Vec, + panic: Option<&Panic>, +) -> Result<()> { + let sentry_upload_url = SENTRY_MINIDUMP_ENDPOINT + .to_owned() + .ok_or_else(|| anyhow::anyhow!("Minidump endpoint not set"))?; + + let mut form = Form::new() + .part( + "upload_file_minidump", + Part::bytes(minidump) + .file_name("minidump.dmp") + .mime_str("application/octet-stream")?, + ) + .text("platform", "rust"); + if let Some(panic) = panic { + form = form.text( + "release", + format!("{}-{}", panic.release_channel, panic.app_version), + ); + // TODO: tack on more fields + } + + let mut response_text = String::new(); + let mut response = http.send_multipart_form(&sentry_upload_url, form).await?; + response + .body_mut() + .read_to_string(&mut response_text) + .await?; + if !response.status().is_success() { + anyhow::bail!("failed to upload minidump: {response_text}"); + } + log::info!("Uploaded minidump. event id: {response_text}"); + Ok(()) +} + async fn upload_panic( http: &Arc, panic_report_url: &Url, diff --git a/docs/src/development/debugging-crashes.md b/docs/src/development/debugging-crashes.md index d08ab961cc..ed0a5807a3 100644 --- a/docs/src/development/debugging-crashes.md +++ b/docs/src/development/debugging-crashes.md @@ -6,6 +6,7 @@ When an app crashes, - macOS creates a `.ips` file in `~/Library/Logs/DiagnosticReports`. You can view these using the built in Console app (`cmd-space Console`) under "Crash Reports". - Linux creates a core dump. See the [man pages](https://man7.org/linux/man-pages/man5/core.5.html) for pointers to how your system might be configured to manage core dumps. +- Windows doesn't create crash reports by default, but can be configured to create "minidump" memory dumps upon applications crashing. If you have enabled Zed's telemetry these will be uploaded to us when you restart the app. They end up in a [Slack channel (internal only)](https://zed-industries.slack.com/archives/C04S6T1T7TQ). diff --git a/docs/src/telemetry.md b/docs/src/telemetry.md index 7f5994be0c..107aef5a96 100644 --- a/docs/src/telemetry.md +++ b/docs/src/telemetry.md @@ -21,7 +21,7 @@ The telemetry settings can also be configured via the welcome screen, which can Telemetry is sent from the application to our servers. Data is proxied through our servers to enable us to easily switch analytics services. We currently use: -- [Axiom](https://axiom.co): Cloud-monitoring service - stores diagnostic events +- [Sentry](https://sentry.io): Crash-monitoring service - stores diagnostic events - [Snowflake](https://snowflake.com): Data warehouse - stores both diagnostic and metric events - [Hex](https://www.hex.tech): Dashboards and data exploration - accesses data stored in Snowflake - [Amplitude](https://www.amplitude.com): Dashboards and data exploration - accesses data stored in Snowflake @@ -30,9 +30,9 @@ Telemetry is sent from the application to our servers. Data is proxied through o ### Diagnostics -Diagnostic events include debug information (stack traces) from crash reports. Reports are sent on the first application launch after the crash occurred. We've built dashboards that allow us to visualize the frequency and severity of issues experienced by users. Having these reports sent automatically allows us to begin implementing fixes without the user needing to file a report in our issue tracker. The plots in the dashboards also give us an informal measurement of the stability of Zed. +Crash reports consist of a [minidump](https://learn.microsoft.com/en-us/windows/win32/debug/minidump-files) and some extra debug information. Reports are sent on the first application launch after the crash occurred. We've built dashboards that allow us to visualize the frequency and severity of issues experienced by users. Having these reports sent automatically allows us to begin implementing fixes without the user needing to file a report in our issue tracker. The plots in the dashboards also give us an informal measurement of the stability of Zed. -You can see what data is sent when a panic occurs by inspecting the `Panic` struct in [crates/telemetry_events/src/telemetry_events.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry_events/src/telemetry_events.rs) in the Zed repo. You can find additional information in the [Debugging Crashes](./development/debugging-crashes.md) documentation. +You can see what extra data is sent alongside the minidump in the `Panic` struct in [crates/telemetry_events/src/telemetry_events.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry_events/src/telemetry_events.rs) in the Zed repo. You can find additional information in the [Debugging Crashes](./development/debugging-crashes.md) documentation. ### Client-Side Usage Data {#client-metrics} diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 4196696f47..5678e46236 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -82,6 +82,7 @@ lyon = { version = "1", default-features = false, features = ["extra"] } lyon_path = { version = "1" } md-5 = { version = "0.10" } memchr = { version = "2" } +mime_guess = { version = "2" } miniz_oxide = { version = "0.8", features = ["simd"] } nom = { version = "7" } num-bigint = { version = "0.4" } @@ -212,6 +213,7 @@ lyon = { version = "1", default-features = false, features = ["extra"] } lyon_path = { version = "1" } md-5 = { version = "0.10" } memchr = { version = "2" } +mime_guess = { version = "2" } miniz_oxide = { version = "0.8", features = ["simd"] } nom = { version = "7" } num-bigint = { version = "0.4" } @@ -290,7 +292,7 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std", hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] } objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } @@ -318,7 +320,7 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std", hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] } objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } @@ -347,7 +349,7 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std", hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] } objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } @@ -375,7 +377,7 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std", hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] } objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } @@ -414,7 +416,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", features = ["span-locations"] } @@ -454,7 +457,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } @@ -492,7 +496,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", features = ["span-locations"] } @@ -532,7 +537,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } @@ -617,7 +623,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", features = ["span-locations"] } @@ -657,7 +664,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } From 6c83a3bcdea1212bc74fc6d46cc6fca869137808 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 4 Aug 2025 18:37:10 -0700 Subject: [PATCH 32/46] Add more information to our logs (#35557) Add more logging to collab in order to help diagnose throughput issues. IMPORTANT: Do not deploy this PR without pinging me. Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- crates/collab/src/rpc.rs | 82 +++++++++++++++++++++------------------- crates/rpc/src/peer.rs | 18 +++++++++ 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e648617fe1..8540a671be 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -315,7 +315,7 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) - .add_request_handler(forward_find_search_candidates_request) + .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) @@ -666,7 +666,6 @@ impl Server { let total_duration_ms = received_at.elapsed().as_micros() as f64 / 1000.0; let processing_duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0; let queue_duration_ms = total_duration_ms - processing_duration_ms; - let payload_type = M::NAME; match result { Err(error) => { @@ -675,7 +674,6 @@ impl Server { total_duration_ms, processing_duration_ms, queue_duration_ms, - payload_type, "error handling message" ) } @@ -780,12 +778,11 @@ impl Server { async move { if *teardown.borrow() { tracing::error!("server is tearing down"); - return + return; } - let (connection_id, handle_io, mut incoming_rx) = this - .peer - .add_connection(connection, { + let (connection_id, handle_io, mut incoming_rx) = + this.peer.add_connection(connection, { let executor = executor.clone(); move |duration| executor.sleep(duration) }); @@ -802,10 +799,14 @@ impl Server { } }; - let supermaven_client = this.app_state.config.supermaven_admin_api_key.clone().map(|supermaven_admin_api_key| Arc::new(SupermavenAdminApi::new( - supermaven_admin_api_key.to_string(), - http_client.clone(), - ))); + let supermaven_client = this.app_state.config.supermaven_admin_api_key.clone().map( + |supermaven_admin_api_key| { + Arc::new(SupermavenAdminApi::new( + supermaven_admin_api_key.to_string(), + http_client.clone(), + )) + }, + ); let session = Session { principal: principal.clone(), @@ -820,7 +821,15 @@ impl Server { supermaven_client, }; - if let Err(error) = this.send_initial_client_update(connection_id, zed_version, send_connection_id, &session).await { + if let Err(error) = this + .send_initial_client_update( + connection_id, + zed_version, + send_connection_id, + &session, + ) + .await + { tracing::error!(?error, "failed to send initial client update"); return; } @@ -837,14 +846,22 @@ impl Server { // // This arrangement ensures we will attempt to process earlier messages first, but fall // back to processing messages arrived later in the spirit of making progress. + const MAX_CONCURRENT_HANDLERS: usize = 256; let mut foreground_message_handlers = FuturesUnordered::new(); - let concurrent_handlers = Arc::new(Semaphore::new(256)); + let concurrent_handlers = Arc::new(Semaphore::new(MAX_CONCURRENT_HANDLERS)); + let get_concurrent_handlers = { + let concurrent_handlers = concurrent_handlers.clone(); + move || MAX_CONCURRENT_HANDLERS - concurrent_handlers.available_permits() + }; loop { let next_message = async { let permit = concurrent_handlers.clone().acquire_owned().await.unwrap(); let message = incoming_rx.next().await; - (permit, message) - }.fuse(); + // Cache the concurrent_handlers here, so that we know what the + // queue looks like as each handler starts + (permit, message, get_concurrent_handlers()) + } + .fuse(); futures::pin_mut!(next_message); futures::select_biased! { _ = teardown.changed().fuse() => return, @@ -856,12 +873,16 @@ impl Server { } _ = foreground_message_handlers.next() => {} next_message = next_message => { - let (permit, message) = next_message; + let (permit, message, concurrent_handlers) = next_message; if let Some(message) = message { let type_name = message.payload_type_name(); // note: we copy all the fields from the parent span so we can query them in the logs. // (https://github.com/tokio-rs/tracing/issues/2670). - let span = tracing::info_span!("receive message", %connection_id, %address, type_name, + let span = tracing::info_span!("receive message", + %connection_id, + %address, + type_name, + concurrent_handlers, user_id=field::Empty, login=field::Empty, impersonator=field::Empty, @@ -895,12 +916,13 @@ impl Server { } drop(foreground_message_handlers); - tracing::info!("signing out"); + let concurrent_handlers = get_concurrent_handlers(); + tracing::info!(concurrent_handlers, "signing out"); if let Err(error) = connection_lost(session, teardown, executor).await { tracing::error!(?error, "error signing out"); } - - }.instrument(span) + } + .instrument(span) } async fn send_initial_client_update( @@ -2286,25 +2308,6 @@ where Ok(()) } -async fn forward_find_search_candidates_request( - request: proto::FindSearchCandidates, - response: Response, - session: Session, -) -> Result<()> { - let project_id = ProjectId::from_proto(request.remote_entity_id()); - let host_connection_id = session - .db() - .await - .host_for_read_only_project_request(project_id, session.connection_id) - .await?; - let payload = session - .peer - .forward_request(session.connection_id, host_connection_id, request) - .await?; - response.send(payload)?; - Ok(()) -} - /// forward a project request to the host. These requests are disallowed /// for guests. async fn forward_mutating_project_request( @@ -2336,6 +2339,7 @@ async fn multi_lsp_query( session: Session, ) -> Result<()> { tracing::Span::current().record("multi_lsp_query_request", request.request_str()); + tracing::info!("multi_lsp_query message received"); forward_mutating_project_request(request, response, session).await } diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 80a104641f..c1fd1df5ff 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -422,8 +422,26 @@ impl Peer { receiver_id: ConnectionId, request: T, ) -> impl Future> { + let request_start_time = Instant::now(); + let payload_type = T::NAME; + let elapsed_time = move || request_start_time.elapsed().as_millis(); + tracing::info!(payload_type, "start forwarding request"); self.request_internal(Some(sender_id), receiver_id, request) .map_ok(|envelope| envelope.payload) + .inspect_err(move |_| { + tracing::error!( + waiting_for_host_ms = elapsed_time(), + payload_type, + "error forwarding request" + ) + }) + .inspect_ok(move |_| { + tracing::info!( + waiting_for_host_ms = elapsed_time(), + payload_type, + "finished forwarding request" + ) + }) } fn request_internal( From efba4364fd28757122489f2564fa6d6b71e717ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Aug 2025 11:33:33 +0200 Subject: [PATCH 33/46] Ensure client reconnects if an error occurs during authentication (#35629) In #35471, we added a new `AuthenticationError` variant to the client enum `Status`, but the reconnection logic was ignoring it when determining whether to reconnect. This pull request fixes that regression and introduces test coverage for this case. Release Notes: - N/A --- crates/client/src/client.rs | 81 +++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 309e4d892f..b4894cddcf 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -687,7 +687,10 @@ impl Client { } } - if matches!(*client.status().borrow(), Status::ConnectionError) { + if matches!( + *client.status().borrow(), + Status::AuthenticationError | Status::ConnectionError + ) { client.set_status( Status::ReconnectionError { next_reconnection: Instant::now() + delay, @@ -856,28 +859,14 @@ impl Client { let old_credentials = self.state.read().credentials.clone(); if let Some(old_credentials) = old_credentials { - if self - .cloud_client - .validate_credentials( - old_credentials.user_id as u32, - &old_credentials.access_token, - ) - .await? - { + if self.validate_credentials(&old_credentials, cx).await? { credentials = Some(old_credentials); } } if credentials.is_none() && try_provider { if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await { - if self - .cloud_client - .validate_credentials( - stored_credentials.user_id as u32, - &stored_credentials.access_token, - ) - .await? - { + if self.validate_credentials(&stored_credentials, cx).await? { credentials = Some(stored_credentials); } else { self.credentials_provider @@ -926,6 +915,24 @@ impl Client { Ok(credentials) } + async fn validate_credentials( + self: &Arc, + credentials: &Credentials, + cx: &AsyncApp, + ) -> Result { + match self + .cloud_client + .validate_credentials(credentials.user_id as u32, &credentials.access_token) + .await + { + Ok(valid) => Ok(valid), + Err(err) => { + self.set_status(Status::AuthenticationError, cx); + Err(anyhow!("failed to validate credentials: {}", err)) + } + } + } + /// Performs a sign-in and also connects to Collab. /// /// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls @@ -1733,6 +1740,46 @@ mod tests { assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token } + #[gpui::test(iterations = 10)] + async fn test_auth_failure_during_reconnection(cx: &mut TestAppContext) { + init_test(cx); + let http_client = FakeHttpClient::with_200_response(); + let client = + cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client.clone(), cx)); + let server = FakeServer::for_client(42, &client, cx).await; + let mut status = client.status(); + assert!(matches!( + status.next().await, + Some(Status::Connected { .. }) + )); + assert_eq!(server.auth_count(), 1); + + // Simulate an auth failure during reconnection. + http_client + .as_fake() + .replace_handler(|_, _request| async move { + Ok(http_client::Response::builder() + .status(503) + .body("".into()) + .unwrap()) + }); + server.disconnect(); + while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} + + // Restore the ability to authenticate. + http_client + .as_fake() + .replace_handler(|_, _request| async move { + Ok(http_client::Response::builder() + .status(200) + .body("".into()) + .unwrap()) + }); + cx.executor().advance_clock(Duration::from_secs(10)); + while !matches!(status.next().await, Some(Status::Connected { .. })) {} + assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting + } + #[gpui::test(iterations = 10)] async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); From 919b888387108a8adbd0e94a24d2a5e47989d34b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:56:49 +0200 Subject: [PATCH 34/46] ruff: Bump to 0.1.1 (#35635) We want Ruff to be built with newer Rust version (as it was built pre-1.84 where we've fixed a bug in std). Closes #35627 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/ruff/Cargo.toml | 2 +- extensions/ruff/extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eae04776d1..a8e3e81434 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20585,7 +20585,7 @@ dependencies = [ [[package]] name = "zed_ruff" -version = "0.1.0" +version = "0.1.1" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/ruff/Cargo.toml b/extensions/ruff/Cargo.toml index 830897279a..24616f963b 100644 --- a/extensions/ruff/Cargo.toml +++ b/extensions/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_ruff" -version = "0.1.0" +version = "0.1.1" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/ruff/extension.toml b/extensions/ruff/extension.toml index 63929fc191..1f5a7314f4 100644 --- a/extensions/ruff/extension.toml +++ b/extensions/ruff/extension.toml @@ -1,7 +1,7 @@ id = "ruff" name = "Ruff" description = "Support for Ruff, the Python linter and formatter" -version = "0.1.0" +version = "0.1.1" schema_version = 1 authors = [] repository = "https://github.com/zed-industries/zed" From 497252480ca40508d2b4778ec25001ad810ed68b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:05:05 -0300 Subject: [PATCH 35/46] agent: Update link to OpenAI compatible docs (#35620) Release Notes: - N/A --- crates/language_models/src/provider/open_ai.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 79ef4a0ee0..5185e979b7 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -868,7 +868,7 @@ impl Render for ConfigurationView { .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { - cx.open_url("https://zed.dev/docs/ai/configuration#openai-api-compatible") + cx.open_url("https://zed.dev/docs/ai/llm-providers#openai-api-compatible") }), ); From 5b40b3618fbca8dc06bcfa101c7d42225c169151 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 5 Aug 2025 09:35:52 -0400 Subject: [PATCH 36/46] Add `workspace::ToggleEditPrediction` for toggling inline completions globally (#35418) Closes: https://github.com/zed-industries/zed/issues/23704 Existing action is `editor::ToggleEditPrediction` ("This Buffer"). This action is `workspace::ToggleEditPredction` ("All Files"). You can add a custom keybind wi shortcut with: ```json { "context": "Workspace", "bindings": { "ctrl-alt-cmd-e": "workspace::ToggleEditPrediction" } }, ``` Screenshot 2025-07-31 at 12 52 19 Release Notes: - Added `workspace::ToggleEditPrediction` action for toggling `show_edit_predictions` in settings (Edit Predictions menu -> All Files). --- .../src/edit_prediction_button.rs | 32 ++++++++----------- crates/workspace/src/workspace.rs | 23 +++++++++++-- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 33165bccf8..9ab94a4095 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -2,11 +2,7 @@ use anyhow::Result; use client::{UserStore, zed_urls}; use cloud_llm_client::UsageLimit; use copilot::{Copilot, Status}; -use editor::{ - Editor, SelectionEffects, - actions::{ShowEditPrediction, ToggleEditPrediction}, - scroll::Autoscroll, -}; +use editor::{Editor, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll}; use feature_flags::{FeatureFlagAppExt, PredictEditsRateCompletionsFeatureFlag}; use fs::Fs; use gpui::{ @@ -441,9 +437,13 @@ impl EditPredictionButton { if let Some(editor_focus_handle) = self.editor_focus_handle.clone() { let entry = ContextMenuEntry::new("This Buffer") .toggleable(IconPosition::Start, self.editor_show_predictions) - .action(Box::new(ToggleEditPrediction)) + .action(Box::new(editor::actions::ToggleEditPrediction)) .handler(move |window, cx| { - editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx); + editor_focus_handle.dispatch_action( + &editor::actions::ToggleEditPrediction, + window, + cx, + ); }); match language_state.clone() { @@ -478,10 +478,13 @@ impl EditPredictionButton { let settings = AllLanguageSettings::get_global(cx); let globally_enabled = settings.show_edit_predictions(None, cx); - menu = menu.toggleable_entry("All Files", globally_enabled, IconPosition::Start, None, { - let fs = fs.clone(); - move |_, cx| toggle_edit_predictions_globally(fs.clone(), cx) - }); + let entry = ContextMenuEntry::new("All Files") + .toggleable(IconPosition::Start, globally_enabled) + .action(workspace::ToggleEditPrediction.boxed_clone()) + .handler(|window, cx| { + window.dispatch_action(workspace::ToggleEditPrediction.boxed_clone(), cx) + }); + menu = menu.item(entry); let provider = settings.edit_predictions.provider; let current_mode = settings.edit_predictions_mode(); @@ -943,13 +946,6 @@ async fn open_disabled_globs_setting_in_editor( anyhow::Ok(()) } -fn toggle_edit_predictions_globally(fs: Arc, cx: &mut App) { - let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); - update_settings_file::(fs, cx, move |file, _| { - file.defaults.show_edit_predictions = Some(!show_edit_predictions) - }); -} - fn set_completion_provider(fs: Arc, cx: &mut App, provider: EditPredictionProvider) { update_settings_file::(fs, cx, move |file, _| { file.features diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6f7db668dd..63953ff802 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -48,7 +48,10 @@ pub use item::{ ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle, }; use itertools::Itertools; -use language::{Buffer, LanguageRegistry, Rope}; +use language::{ + Buffer, LanguageRegistry, Rope, + language_settings::{AllLanguageSettings, all_language_settings}, +}; pub use modal_layer::*; use node_runtime::NodeRuntime; use notifications::{ @@ -74,7 +77,7 @@ use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIde use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; -use settings::Settings; +use settings::{Settings, update_settings_file}; use shared_screen::SharedScreen; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -233,6 +236,8 @@ actions!( ToggleBottomDock, /// Toggles centered layout mode. ToggleCenteredLayout, + /// Toggles edit prediction feature globally for all files. + ToggleEditPrediction, /// Toggles the left dock. ToggleLeftDock, /// Toggles the right dock. @@ -5546,6 +5551,7 @@ impl Workspace { .on_action(cx.listener(Self::activate_pane_at_index)) .on_action(cx.listener(Self::move_item_to_pane_at_index)) .on_action(cx.listener(Self::move_focused_panel_to_next_position)) + .on_action(cx.listener(Self::toggle_edit_predictions_all_files)) .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| { let pane = workspace.active_pane().clone(); workspace.unfollow_in_pane(&pane, window, cx); @@ -5977,6 +5983,19 @@ impl Workspace { } }); } + + fn toggle_edit_predictions_all_files( + &mut self, + _: &ToggleEditPrediction, + _window: &mut Window, + cx: &mut Context, + ) { + let fs = self.project().read(cx).fs().clone(); + let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); + update_settings_file::(fs, cx, move |file, _| { + file.defaults.show_edit_predictions = Some(!show_edit_predictions) + }); + } } fn leader_border_for_pane( From 22473fc6119d41a63714cde9d0139c7a943c7dd8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 5 Aug 2025 16:36:05 +0300 Subject: [PATCH 37/46] Stop sending redundant LSP proto requests (#35581) Before, each time any LSP feature was used on client remote, it always produced a `proto::` request that always had been sent to the host, from where returned as an empty response. Instead, propagate more language server-related data to the client, `lsp::ServerCapability`, so Zed client can omit certain requests if those are not supported. On top of that, rework the approach Zed uses to query for the data refreshes: before, editors tried to fetch the data when the server start was reported (locally and remotely). Now, a later event is selected: on each `textDocument/didOpen` for the buffer contained in this editor, we will query for new LSP data, reusing the cache if needed. Before, servers could reject unregistered files' LSP queries, or process them slowly when starting up. Now, such refreshes are happening later and should be cached. This requires a collab DB change, to restore server data on rejoin. Release Notes: - Fixed excessive LSP requests sent during remote sessions --- .../20221109000000_test_schema.sql | 1 + ...804080620_language_server_capabilities.sql | 5 + crates/collab/src/db.rs | 19 +- crates/collab/src/db/queries/buffers.rs | 26 + crates/collab/src/db/queries/projects.rs | 12 +- crates/collab/src/db/queries/rooms.rs | 11 +- .../collab/src/db/tables/language_server.rs | 1 + crates/collab/src/rpc.rs | 27 +- crates/collab/src/tests/editor_tests.rs | 192 ++++--- crates/collab/src/tests/integration_tests.rs | 160 +++++- crates/editor/src/editor.rs | 10 +- crates/editor/src/linked_editing_ranges.rs | 2 +- crates/language/src/language_registry.rs | 24 - crates/project/src/lsp_command.rs | 45 +- crates/project/src/lsp_store.rs | 507 ++++++++++++------ crates/project/src/project.rs | 80 +-- crates/project/src/project_tests.rs | 13 +- crates/proto/proto/call.proto | 2 + crates/proto/proto/lsp.proto | 7 + 19 files changed, 793 insertions(+), 351 deletions(-) create mode 100644 crates/collab/migrations/20250804080620_language_server_capabilities.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index ca840493ad..73d473ab76 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -173,6 +173,7 @@ CREATE TABLE "language_servers" ( "id" INTEGER NOT NULL, "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, "name" VARCHAR NOT NULL, + "capabilities" TEXT NOT NULL, PRIMARY KEY (project_id, id) ); diff --git a/crates/collab/migrations/20250804080620_language_server_capabilities.sql b/crates/collab/migrations/20250804080620_language_server_capabilities.sql new file mode 100644 index 0000000000..f74f094ed2 --- /dev/null +++ b/crates/collab/migrations/20250804080620_language_server_capabilities.sql @@ -0,0 +1,5 @@ +ALTER TABLE language_servers + ADD COLUMN capabilities TEXT NOT NULL DEFAULT '{}'; + +ALTER TABLE language_servers + ALTER COLUMN capabilities DROP DEFAULT; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 8cd1e3ea83..2c22ca2069 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -529,11 +529,17 @@ pub struct RejoinedProject { pub worktrees: Vec, pub updated_repositories: Vec, pub removed_repositories: Vec, - pub language_servers: Vec, + pub language_servers: Vec, } impl RejoinedProject { pub fn to_proto(&self) -> proto::RejoinedProject { + let (language_servers, language_server_capabilities) = self + .language_servers + .clone() + .into_iter() + .map(|server| (server.server, server.capabilities)) + .unzip(); proto::RejoinedProject { id: self.id.to_proto(), worktrees: self @@ -551,7 +557,8 @@ impl RejoinedProject { .iter() .map(|collaborator| collaborator.to_proto()) .collect(), - language_servers: self.language_servers.clone(), + language_servers, + language_server_capabilities, } } } @@ -598,7 +605,7 @@ pub struct Project { pub collaborators: Vec, pub worktrees: BTreeMap, pub repositories: Vec, - pub language_servers: Vec, + pub language_servers: Vec, } pub struct ProjectCollaborator { @@ -623,6 +630,12 @@ impl ProjectCollaborator { } } +#[derive(Debug, Clone)] +pub struct LanguageServer { + pub server: proto::LanguageServer, + pub capabilities: String, +} + #[derive(Debug)] pub struct LeftProject { pub id: ProjectId, diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index a288a4e7eb..2e6b4719d1 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -786,6 +786,32 @@ impl Database { }) .collect()) } + + /// Update language server capabilities for a given id. + pub async fn update_server_capabilities( + &self, + project_id: ProjectId, + server_id: u64, + new_capabilities: String, + ) -> Result<()> { + self.transaction(|tx| { + let new_capabilities = new_capabilities.clone(); + async move { + Ok( + language_server::Entity::update(language_server::ActiveModel { + project_id: ActiveValue::unchanged(project_id), + id: ActiveValue::unchanged(server_id as i64), + capabilities: ActiveValue::set(new_capabilities), + ..Default::default() + }) + .exec(&*tx) + .await?, + ) + } + }) + .await?; + Ok(()) + } } fn operation_to_storage( diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index ba22a7b4e3..31635575a8 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -692,6 +692,7 @@ impl Database { project_id: ActiveValue::set(project_id), id: ActiveValue::set(server.id as i64), name: ActiveValue::set(server.name.clone()), + capabilities: ActiveValue::set(update.capabilities.clone()), }) .on_conflict( OnConflict::columns([ @@ -1054,10 +1055,13 @@ impl Database { repositories, language_servers: language_servers .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - worktree_id: None, + .map(|language_server| LanguageServer { + server: proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + worktree_id: None, + }, + capabilities: language_server.capabilities, }) .collect(), }; diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index cb805786dd..c63d7133be 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -804,10 +804,13 @@ impl Database { .all(tx) .await? .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - worktree_id: None, + .map(|language_server| LanguageServer { + server: proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + worktree_id: None, + }, + capabilities: language_server.capabilities, }) .collect::>(); diff --git a/crates/collab/src/db/tables/language_server.rs b/crates/collab/src/db/tables/language_server.rs index 9ff8c75fc6..34c7514d91 100644 --- a/crates/collab/src/db/tables/language_server.rs +++ b/crates/collab/src/db/tables/language_server.rs @@ -9,6 +9,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: i64, pub name: String, + pub capabilities: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8540a671be..22b21f2c7a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1990,12 +1990,19 @@ async fn join_project( } // First, we send the metadata associated with each worktree. + let (language_servers, language_server_capabilities) = project + .language_servers + .clone() + .into_iter() + .map(|server| (server.server, server.capabilities)) + .unzip(); response.send(proto::JoinProjectResponse { project_id: project.id.0 as u64, worktrees: worktrees.clone(), replica_id: replica_id.0 as u32, collaborators: collaborators.clone(), - language_servers: project.language_servers.clone(), + language_servers, + language_server_capabilities, role: project.role.into(), })?; @@ -2054,8 +2061,8 @@ async fn join_project( session.connection_id, proto::UpdateLanguageServer { project_id: project_id.to_proto(), - server_name: Some(language_server.name.clone()), - language_server_id: language_server.id, + server_name: Some(language_server.server.name.clone()), + language_server_id: language_server.server.id, variant: Some( proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( proto::LspDiskBasedDiagnosticsUpdated {}, @@ -2267,9 +2274,17 @@ async fn update_language_server( session: Session, ) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await + let db = session.db().await; + + if let Some(proto::update_language_server::Variant::MetadataUpdated(update)) = &request.variant + { + if let Some(capabilities) = update.capabilities.clone() { + db.update_server_capabilities(project_id, request.language_server_id, capabilities) + .await?; + } + } + + let project_connection_ids = db .project_connection_ids(project_id, session.connection_id, true) .await?; broadcast( diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 73ab2b8167..1d28c7f6ef 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -296,19 +296,28 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .await; let active_call_a = cx_a.read(ActiveCall::global); + let capabilities = lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..lsp::CompletionOptions::default() + }), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -566,11 +575,14 @@ async fn test_collaborating_with_code_actions( cx_b.update(editor::init); - // Set up a fake language server. client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a .language_registry() .register_fake_lsp("Rust", FakeLspAdapter::default()); + client_b.language_registry().add(rust_lang()); + client_b + .language_registry() + .register_fake_lsp("Rust", FakeLspAdapter::default()); client_a .fs() @@ -775,19 +787,27 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T cx_b.update(editor::init); - // Set up a fake language server. + let capabilities = lsp::ServerCapabilities { + rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), + })), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { - prepare_provider: Some(true), - work_done_progress_options: Default::default(), - })), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -818,6 +838,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T .downcast::() .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); // Move cursor to a location that can be renamed. let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| { @@ -1055,7 +1077,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes project_a.read_with(cx_a, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "the-language-server"); + assert_eq!(status.name.0, "the-language-server"); assert_eq!(status.pending_work.len(), 1); assert_eq!( status.pending_work["the-token"].message.as_ref().unwrap(), @@ -1072,7 +1094,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes project_b.read_with(cx_b, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "the-language-server"); + assert_eq!(status.name.0, "the-language-server"); }); executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT); @@ -1089,7 +1111,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes project_a.read_with(cx_a, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "the-language-server"); + assert_eq!(status.name.0, "the-language-server"); assert_eq!(status.pending_work.len(), 1); assert_eq!( status.pending_work["the-token"].message.as_ref().unwrap(), @@ -1099,7 +1121,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes project_b.read_with(cx_b, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "the-language-server"); + assert_eq!(status.name.0, "the-language-server"); assert_eq!(status.pending_work.len(), 1); assert_eq!( status.pending_work["the-token"].message.as_ref().unwrap(), @@ -1422,18 +1444,27 @@ async fn test_on_input_format_from_guest_to_host( .await; let active_call_a = cx_a.read(ActiveCall::global); + let capabilities = lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: ":".to_string(), - more_trigger_character: Some(vec![">".to_string()]), - }), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -1588,16 +1619,24 @@ async fn test_mutual_editor_inlay_hint_cache_update( }); }); + let capabilities = lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - client_b.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -1830,16 +1869,24 @@ async fn test_inlay_hint_refresh_is_forwarded( }); }); + let capabilities = lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - client_b.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -2004,15 +2051,23 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo }); }); + let capabilities = lsp::ServerCapabilities { + color_provider: Some(lsp::ColorProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - client_b.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - color_provider: Some(lsp::ColorProviderCapability::Simple(true)), - ..lsp::ServerCapabilities::default() - }, + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, ..FakeLspAdapter::default() }, ); @@ -2063,6 +2118,8 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let requests_made = Arc::new(AtomicUsize::new(0)); let closure_requests_made = Arc::clone(&requests_made); @@ -2264,24 +2321,32 @@ async fn test_lsp_pull_diagnostics( cx_a.update(editor::init); cx_b.update(editor::init); + let capabilities = lsp::ServerCapabilities { + diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options( + lsp::DiagnosticOptions { + identifier: Some("test-pulls".to_string()), + inter_file_dependencies: true, + workspace_diagnostics: true, + work_done_progress_options: lsp::WorkDoneProgressOptions { + work_done_progress: None, + }, + }, + )), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - client_b.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options( - lsp::DiagnosticOptions { - identifier: Some("test-pulls".to_string()), - inter_file_dependencies: true, - workspace_diagnostics: true, - work_done_progress_options: lsp::WorkDoneProgressOptions { - work_done_progress: None, - }, - }, - )), - ..lsp::ServerCapabilities::default() - }, + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, ..FakeLspAdapter::default() }, ); @@ -2334,6 +2399,8 @@ async fn test_lsp_pull_diagnostics( .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let expected_push_diagnostic_main_message = "pushed main diagnostic"; let expected_push_diagnostic_lib_message = "pushed lib diagnostic"; let expected_pull_diagnostic_main_message = "pulled main diagnostic"; @@ -2689,6 +2756,7 @@ async fn test_lsp_pull_diagnostics( .unwrap() .downcast::() .unwrap(); + cx_b.run_until_parked(); pull_diagnostics_handle.next().await.unwrap(); assert_eq!( diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index aea359d75b..5a2c40b890 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4778,10 +4778,27 @@ async fn test_definition( .await; let active_call_a = cx_a.read(ActiveCall::global); - let mut fake_language_servers = client_a - .language_registry() - .register_fake_lsp("Rust", Default::default()); + let capabilities = lsp::ServerCapabilities { + definition_provider: Some(OneOf::Left(true)), + type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() + }, + ); client_a .fs() @@ -4827,13 +4844,19 @@ async fn test_definition( ))) }, ); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let definitions_1 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx)) .await .unwrap(); cx_b.read(|cx| { - assert_eq!(definitions_1.len(), 1); + assert_eq!( + definitions_1.len(), + 1, + "Unexpected definitions: {definitions_1:?}" + ); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); let target_buffer = definitions_1[0].target.buffer.read(cx); assert_eq!( @@ -4901,7 +4924,11 @@ async fn test_definition( .await .unwrap(); cx_b.read(|cx| { - assert_eq!(type_definitions.len(), 1); + assert_eq!( + type_definitions.len(), + 1, + "Unexpected type definitions: {type_definitions:?}" + ); let target_buffer = type_definitions[0].target.buffer.read(cx); assert_eq!(target_buffer.text(), "type T2 = usize;"); assert_eq!( @@ -4925,16 +4952,26 @@ async fn test_references( .await; let active_call_a = cx_a.read(ActiveCall::global); + let capabilities = lsp::ServerCapabilities { + references_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { name: "my-fake-lsp-adapter", - capabilities: lsp::ServerCapabilities { - references_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "my-fake-lsp-adapter", + capabilities: capabilities, + ..FakeLspAdapter::default() }, ); @@ -4989,6 +5026,8 @@ async fn test_references( } } }); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx)); @@ -4996,7 +5035,7 @@ async fn test_references( executor.run_until_parked(); project_b.read_with(cx_b, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "my-fake-lsp-adapter"); + assert_eq!(status.name.0, "my-fake-lsp-adapter"); assert_eq!( status.pending_work.values().next().unwrap().message, Some("Finding references...".into()) @@ -5054,7 +5093,7 @@ async fn test_references( executor.run_until_parked(); project_b.read_with(cx_b, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "my-fake-lsp-adapter"); + assert_eq!(status.name.0, "my-fake-lsp-adapter"); assert_eq!( status.pending_work.values().next().unwrap().message, Some("Finding references...".into()) @@ -5204,10 +5243,26 @@ async fn test_document_highlights( ) .await; - let mut fake_language_servers = client_a - .language_registry() - .register_fake_lsp("Rust", Default::default()); client_a.language_registry().add(rust_lang()); + let capabilities = lsp::ServerCapabilities { + document_highlight_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; + let mut fake_language_servers = client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() + }, + ); let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await; let project_id = active_call_a @@ -5256,6 +5311,8 @@ async fn test_document_highlights( ])) }, ); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let highlights = project_b .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx)) @@ -5306,30 +5363,49 @@ async fn test_lsp_hover( client_a.language_registry().add(rust_lang()); let language_server_names = ["rust-analyzer", "CrabLang-ls"]; + let capabilities_1 = lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }; + let capabilities_2 = lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }; let mut language_servers = [ client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - name: "rust-analyzer", - capabilities: lsp::ServerCapabilities { - hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), - ..lsp::ServerCapabilities::default() - }, + name: language_server_names[0], + capabilities: capabilities_1.clone(), ..FakeLspAdapter::default() }, ), client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - name: "CrabLang-ls", - capabilities: lsp::ServerCapabilities { - hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), - ..lsp::ServerCapabilities::default() - }, + name: language_server_names[1], + capabilities: capabilities_2.clone(), ..FakeLspAdapter::default() }, ), ]; + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: language_server_names[0], + capabilities: capabilities_1, + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: language_server_names[1], + capabilities: capabilities_2, + ..FakeLspAdapter::default() + }, + ); let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await; let project_id = active_call_a @@ -5423,6 +5499,8 @@ async fn test_lsp_hover( unexpected => panic!("Unexpected server name: {unexpected}"), } } + cx_a.run_until_parked(); + cx_b.run_until_parked(); // Request hover information as the guest. let mut hovers = project_b @@ -5605,10 +5683,26 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( .await; let active_call_a = cx_a.read(ActiveCall::global); + let capabilities = lsp::ServerCapabilities { + definition_provider: Some(OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - let mut fake_language_servers = client_a - .language_registry() - .register_fake_lsp("Rust", Default::default()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() + }, + ); client_a .fs() @@ -5649,6 +5743,8 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( let definitions; let buffer_b2; if rng.r#gen() { + cx_a.run_until_parked(); + cx_b.run_until_parked(); definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx)); (buffer_b2, _) = project_b .update(cx_b, |p, cx| { @@ -5663,11 +5759,17 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( }) .await .unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx)); } let definitions = definitions.await.unwrap(); - assert_eq!(definitions.len(), 1); + assert_eq!( + definitions.len(), + 1, + "Unexpected definitions: {definitions:?}" + ); assert_eq!(definitions[0].target.buffer, buffer_b2); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2912708b56..ff9b703d66 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -134,7 +134,7 @@ use language::{ use linked_editing_ranges::refresh_linked_ranges; use lsp::{ CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode, - LanguageServerId, LanguageServerName, + LanguageServerId, }; use lsp_colors::LspColorData; use markdown::Markdown; @@ -1864,7 +1864,6 @@ impl Editor { editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); } - editor.update_lsp_data(true, None, window, cx); } project::Event::SnippetEdit(id, snippet_edits) => { if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { @@ -1886,6 +1885,11 @@ impl Editor { } } } + project::Event::LanguageServerBufferRegistered { buffer_id, .. } => { + if editor.buffer().read(cx).buffer(*buffer_id).is_some() { + editor.update_lsp_data(false, Some(*buffer_id), window, cx); + } + } _ => {} }, )); @@ -15846,7 +15850,7 @@ impl Editor { let language_server_name = project .language_server_statuses(cx) .find(|(id, _)| server_id == *id) - .map(|(_, status)| LanguageServerName::from(status.name.as_str())); + .map(|(_, status)| status.name.clone()); language_server_name.map(|language_server_name| { project.open_local_buffer_via_lsp( lsp_location.uri.clone(), diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs index 7c2672fc0d..a185de33ca 100644 --- a/crates/editor/src/linked_editing_ranges.rs +++ b/crates/editor/src/linked_editing_ranges.rs @@ -95,7 +95,7 @@ pub(super) fn refresh_linked_ranges( let snapshot = buffer.read(cx).snapshot(); let buffer_id = buffer.read(cx).remote_id(); - let linked_edits_task = project.linked_edit(buffer, *start, cx); + let linked_edits_task = project.linked_edits(buffer, *start, cx); let highlights = move || async move { let edits = linked_edits_task.await.log_err()?; // Find the range containing our current selection. diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index ab3c0f9b37..85123d2373 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -411,30 +411,6 @@ impl LanguageRegistry { cached } - pub fn get_or_register_lsp_adapter( - &self, - language_name: LanguageName, - server_name: LanguageServerName, - build_adapter: impl FnOnce() -> Arc + 'static, - ) -> Arc { - let registered = self - .state - .write() - .lsp_adapters - .entry(language_name.clone()) - .or_default() - .iter() - .find(|cached_adapter| cached_adapter.name == server_name) - .cloned(); - - if let Some(found) = registered { - found - } else { - let adapter = build_adapter(); - self.register_lsp_adapter(language_name, adapter) - } - } - /// Register a fake language server and adapter /// The returned channel receives a new instance of the language server every time it is started #[cfg(any(feature = "test-support", test))] diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 2fd61ea0b2..f8e69e2185 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2154,6 +2154,16 @@ impl LspCommand for GetHover { } } +impl GetCompletions { + pub fn can_resolve_completions(capabilities: &lsp::ServerCapabilities) -> bool { + capabilities + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false) + } +} + #[async_trait(?Send)] impl LspCommand for GetCompletions { type Response = CoreCompletionResponse; @@ -2762,6 +2772,23 @@ impl GetCodeActions { } } +impl OnTypeFormatting { + pub fn supports_on_type_formatting(trigger: &str, capabilities: &ServerCapabilities) -> bool { + let Some(on_type_formatting_options) = &capabilities.document_on_type_formatting_provider + else { + return false; + }; + on_type_formatting_options + .first_trigger_character + .contains(trigger) + || on_type_formatting_options + .more_trigger_character + .iter() + .flatten() + .any(|chars| chars.contains(trigger)) + } +} + #[async_trait(?Send)] impl LspCommand for OnTypeFormatting { type Response = Option; @@ -2773,20 +2800,7 @@ impl LspCommand for OnTypeFormatting { } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - let Some(on_type_formatting_options) = &capabilities - .server_capabilities - .document_on_type_formatting_provider - else { - return false; - }; - on_type_formatting_options - .first_trigger_character - .contains(&self.trigger) - || on_type_formatting_options - .more_trigger_character - .iter() - .flatten() - .any(|chars| chars.contains(&self.trigger)) + Self::supports_on_type_formatting(&self.trigger, &capabilities.server_capabilities) } fn to_lsp( @@ -4221,8 +4235,9 @@ impl LspCommand for GetDocumentColor { server_capabilities .server_capabilities .color_provider + .as_ref() .is_some_and(|capability| match capability { - lsp::ColorProviderCapability::Simple(supported) => supported, + lsp::ColorProviderCapability::Simple(supported) => *supported, lsp::ColorProviderCapability::ColorProvider(..) => true, lsp::ColorProviderCapability::Options(..) => true, }) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 6122204991..6d448a6fea 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -58,12 +58,12 @@ use language::{ range_from_lsp, range_to_lsp, }; use lsp::{ - CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, - DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, - FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer, - LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, - LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf, - RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, + AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity, + DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, + FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, + LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, + LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, + OneOf, RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; @@ -622,7 +622,7 @@ impl LocalLspStore { .on_request::({ let this = this.clone(); move |params, cx| { - let this = this.clone(); + let lsp_store = this.clone(); let mut cx = cx.clone(); async move { for reg in params.registrations { @@ -630,7 +630,7 @@ impl LocalLspStore { "workspace/didChangeWatchedFiles" => { if let Some(options) = reg.register_options { let options = serde_json::from_value(options)?; - this.update(&mut cx, |this, cx| { + lsp_store.update(&mut cx, |this, cx| { this.as_local_mut()?.on_lsp_did_change_watched_files( server_id, ®.id, options, cx, ); @@ -639,8 +639,9 @@ impl LocalLspStore { } } "textDocument/rangeFormatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { let options = reg .register_options @@ -659,14 +660,16 @@ impl LocalLspStore { server.update_capabilities(|capabilities| { capabilities.document_range_formatting_provider = Some(provider); - }) + }); + notify_server_capabilities_updated(&server, cx); } anyhow::Ok(()) })??; } "textDocument/onTypeFormatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { let options = reg .register_options @@ -683,15 +686,17 @@ impl LocalLspStore { capabilities .document_on_type_formatting_provider = Some(options); - }) + }); + notify_server_capabilities_updated(&server, cx); } } anyhow::Ok(()) })??; } "textDocument/formatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { let options = reg .register_options @@ -710,7 +715,8 @@ impl LocalLspStore { server.update_capabilities(|capabilities| { capabilities.document_formatting_provider = Some(provider); - }) + }); + notify_server_capabilities_updated(&server, cx); } anyhow::Ok(()) })??; @@ -719,8 +725,9 @@ impl LocalLspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "textDocument/rename" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { let options = reg .register_options @@ -737,7 +744,8 @@ impl LocalLspStore { server.update_capabilities(|capabilities| { capabilities.rename_provider = Some(options); - }) + }); + notify_server_capabilities_updated(&server, cx); } anyhow::Ok(()) })??; @@ -755,14 +763,15 @@ impl LocalLspStore { .on_request::({ let this = this.clone(); move |params, cx| { - let this = this.clone(); + let lsp_store = this.clone(); let mut cx = cx.clone(); async move { for unreg in params.unregisterations.iter() { match unreg.method.as_str() { "workspace/didChangeWatchedFiles" => { - this.update(&mut cx, |this, cx| { - this.as_local_mut()? + lsp_store.update(&mut cx, |lsp_store, cx| { + lsp_store + .as_local_mut()? .on_lsp_unregister_did_change_watched_files( server_id, &unreg.id, cx, ); @@ -773,44 +782,52 @@ impl LocalLspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "textDocument/rename" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { server.update_capabilities(|capabilities| { capabilities.rename_provider = None - }) + }); + notify_server_capabilities_updated(&server, cx); } })?; } "textDocument/rangeFormatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { server.update_capabilities(|capabilities| { capabilities.document_range_formatting_provider = None - }) + }); + notify_server_capabilities_updated(&server, cx); } })?; } "textDocument/onTypeFormatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { server.update_capabilities(|capabilities| { capabilities.document_on_type_formatting_provider = None; - }) + }); + notify_server_capabilities_updated(&server, cx); } })?; } "textDocument/formatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { server.update_capabilities(|capabilities| { capabilities.document_formatting_provider = None; - }) + }); + notify_server_capabilities_updated(&server, cx); } })?; } @@ -2426,7 +2443,6 @@ impl LocalLspStore { let server_id = server_node.server_id_or_init( |LaunchDisposition { server_name, - path, settings, }| { @@ -2468,18 +2484,6 @@ impl LocalLspStore { } }; - let lsp_store = self.weak.clone(); - let server_name = server_node.name(); - let buffer_abs_path = abs_path.to_string_lossy().to_string(); - cx.defer(move |cx| { - lsp_store.update(cx, |_, cx| cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server_id, - name: server_name, - message: proto::update_language_server::Variant::RegisteredForBuffer(proto::RegisteredForBuffer { - buffer_abs_path, - }) - })).ok(); - }); server_id }, )?; @@ -2515,11 +2519,13 @@ impl LocalLspStore { snapshot: initial_snapshot.clone(), }; + let mut registered = false; self.buffer_snapshots .entry(buffer_id) .or_default() .entry(server.server_id()) .or_insert_with(|| { + registered = true; server.register_buffer( uri.clone(), adapter.language_id(&language.name()), @@ -2534,15 +2540,18 @@ impl LocalLspStore { .entry(buffer_id) .or_default() .insert(server.server_id()); - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server.server_id(), - name: None, - message: proto::update_language_server::Variant::RegisteredForBuffer( - proto::RegisteredForBuffer { - buffer_abs_path: abs_path.to_string_lossy().to_string(), - }, - ), - }); + if registered { + cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id: server.server_id(), + name: None, + message: proto::update_language_server::Variant::RegisteredForBuffer( + proto::RegisteredForBuffer { + buffer_abs_path: abs_path.to_string_lossy().to_string(), + buffer_id: buffer_id.to_proto(), + }, + ), + }); + } } } @@ -3494,6 +3503,20 @@ impl LocalLspStore { } } +fn notify_server_capabilities_updated(server: &LanguageServer, cx: &mut Context) { + if let Some(capabilities) = serde_json::to_string(&server.capabilities()).ok() { + cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id: server.server_id(), + name: Some(server.name()), + message: proto::update_language_server::Variant::MetadataUpdated( + proto::ServerMetadataUpdated { + capabilities: Some(capabilities), + }, + ), + }); + } +} + #[derive(Debug)] pub struct FormattableBuffer { handle: Entity, @@ -3533,6 +3556,7 @@ pub struct LspStore { _maintain_buffer_languages: Task<()>, diagnostic_summaries: HashMap, HashMap>>, + pub(super) lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, } @@ -3604,7 +3628,7 @@ pub enum LspStoreEvent { #[derive(Clone, Debug, Serialize)] pub struct LanguageServerStatus { - pub name: String, + pub name: LanguageServerName, pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, progress_tokens: HashSet, @@ -3795,6 +3819,7 @@ impl LspStore { language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), diagnostic_summaries: HashMap::default(), + lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), active_entry: None, @@ -3811,6 +3836,9 @@ impl LspStore { request: R, cx: &mut Context, ) -> Task::Response>> { + if !self.is_capable_for_proto_request(&buffer, &request, cx) { + return Task::ready(Ok(R::Response::default())); + } let message = request.to_proto(upstream_project_id, buffer.read(cx)); cx.spawn(async move |this, cx| { let response = client.request(message).await?; @@ -3853,6 +3881,7 @@ impl LspStore { language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), diagnostic_summaries: HashMap::default(), + lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), active_entry: None, @@ -4428,20 +4457,73 @@ impl LspStore { } } - pub fn request_lsp( + // TODO: remove MultiLspQuery: instead, the proto handler should pick appropriate server(s) + // Then, use `send_lsp_proto_request` or analogue for most of the LSP proto requests and inline this check inside + fn is_capable_for_proto_request( + &self, + buffer: &Entity, + request: &R, + cx: &Context, + ) -> bool + where + R: LspCommand, + { + self.check_if_capable_for_proto_request( + buffer, + |capabilities| { + request.check_capabilities(AdapterServerCapabilities { + server_capabilities: capabilities.clone(), + code_action_kinds: None, + }) + }, + cx, + ) + } + + fn check_if_capable_for_proto_request( + &self, + buffer: &Entity, + check: F, + cx: &Context, + ) -> bool + where + F: Fn(&lsp::ServerCapabilities) -> bool, + { + let Some(language) = buffer.read(cx).language().cloned() else { + return false; + }; + let relevant_language_servers = self + .languages + .lsp_adapters(&language.name()) + .into_iter() + .map(|lsp_adapter| lsp_adapter.name()) + .collect::>(); + self.language_server_statuses + .iter() + .filter_map(|(server_id, server_status)| { + relevant_language_servers + .contains(&server_status.name) + .then_some(server_id) + }) + .filter_map(|server_id| self.lsp_server_capabilities.get(&server_id)) + .any(check) + } + + pub fn request_lsp( &mut self, - buffer_handle: Entity, + buffer: Entity, server: LanguageServerToQuery, request: R, cx: &mut Context, ) -> Task> where + R: LspCommand, ::Result: Send, ::Params: Send, { if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { return self.send_lsp_proto_request( - buffer_handle, + buffer, upstream_client, upstream_project_id, request, @@ -4449,7 +4531,7 @@ impl LspStore { ); } - let Some(language_server) = buffer_handle.update(cx, |buffer, cx| match server { + let Some(language_server) = buffer.update(cx, |buffer, cx| match server { LanguageServerToQuery::FirstCapable => self.as_local().and_then(|local| { local .language_servers_for_buffer(buffer, cx) @@ -4469,8 +4551,7 @@ impl LspStore { return Task::ready(Ok(Default::default())); }; - let buffer = buffer_handle.read(cx); - let file = File::from_dyn(buffer.file()).and_then(File::as_local); + let file = File::from_dyn(buffer.read(cx).file()).and_then(File::as_local); let Some(file) = file else { return Task::ready(Ok(Default::default())); @@ -4478,7 +4559,7 @@ impl LspStore { let lsp_params = match request.to_lsp_params_or_response( &file.abs_path(cx), - buffer, + buffer.read(cx), &language_server, cx, ) { @@ -4554,7 +4635,7 @@ impl LspStore { .response_from_lsp( response, this.upgrade().context("no app context")?, - buffer_handle, + buffer, language_server.server_id(), cx.clone(), ) @@ -4624,7 +4705,8 @@ impl LspStore { ) }) { let buffer = buffer_handle.read(cx); - if !local.registered_buffers.contains_key(&buffer.remote_id()) { + let buffer_id = buffer.remote_id(); + if !local.registered_buffers.contains_key(&buffer_id) { continue; } if let Some((file, language)) = File::from_dyn(buffer.file()) @@ -4725,6 +4807,7 @@ impl LspStore { proto::update_language_server::Variant::RegisteredForBuffer( proto::RegisteredForBuffer { buffer_abs_path: abs_path.to_string_lossy().to_string(), + buffer_id: buffer_id.to_proto(), }, ), }); @@ -4900,15 +4983,20 @@ impl LspStore { pub fn resolve_inlay_hint( &self, - hint: InlayHint, - buffer_handle: Entity, + mut hint: InlayHint, + buffer: Entity, server_id: LanguageServerId, cx: &mut Context, ) -> Task> { if let Some((upstream_client, project_id)) = self.upstream_client() { + if !self.check_if_capable_for_proto_request(&buffer, InlayHints::can_resolve_inlays, cx) + { + hint.resolve_state = ResolveState::Resolved; + return Task::ready(Ok(hint)); + } let request = proto::ResolveInlayHint { project_id, - buffer_id: buffer_handle.read(cx).remote_id().into(), + buffer_id: buffer.read(cx).remote_id().into(), language_server_id: server_id.0 as u64, hint: Some(InlayHints::project_to_proto_hint(hint.clone())), }; @@ -4924,7 +5012,7 @@ impl LspStore { } }) } else { - let Some(lang_server) = buffer_handle.update(cx, |buffer, cx| { + let Some(lang_server) = buffer.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, server_id, cx) .map(|(_, server)| server.clone()) }) else { @@ -4933,7 +5021,7 @@ impl LspStore { if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) { return Task::ready(Ok(hint)); } - let buffer_snapshot = buffer_handle.read(cx).snapshot(); + let buffer_snapshot = buffer.read(cx).snapshot(); cx.spawn(async move |_, cx| { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), @@ -4944,7 +5032,7 @@ impl LspStore { .context("inlay hint resolve LSP request")?; let resolved_hint = InlayHints::lsp_to_project_hint( resolved_hint, - &buffer_handle, + &buffer, server_id, ResolveState::Resolved, false, @@ -5055,7 +5143,7 @@ impl LspStore { } } - pub(crate) fn linked_edit( + pub(crate) fn linked_edits( &mut self, buffer: &Entity, position: Anchor, @@ -5097,7 +5185,7 @@ impl LspStore { }) == Some(true) }) else { - return Task::ready(Ok(vec![])); + return Task::ready(Ok(Vec::new())); }; self.request_lsp( @@ -5116,6 +5204,15 @@ impl LspStore { cx: &mut Context, ) -> Task>> { if let Some((client, project_id)) = self.upstream_client() { + if !self.check_if_capable_for_proto_request( + &buffer, + |capabilities| { + OnTypeFormatting::supports_on_type_formatting(&trigger, capabilities) + }, + cx, + ) { + return Task::ready(Ok(None)); + } let request = proto::OnTypeFormatting { project_id, buffer_id: buffer.read(cx).remote_id().into(), @@ -5227,6 +5324,10 @@ impl LspStore { cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetDefinitions { position }; + if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { buffer_id: buffer_handle.read(cx).remote_id().into(), version: serialize_version(&buffer_handle.read(cx).version()), @@ -5235,7 +5336,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetDefinition( - GetDefinitions { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer_handle.read(cx)), )), }); let buffer = buffer_handle.clone(); @@ -5300,6 +5401,10 @@ impl LspStore { cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetDeclarations { position }; + if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { buffer_id: buffer_handle.read(cx).remote_id().into(), version: serialize_version(&buffer_handle.read(cx).version()), @@ -5308,7 +5413,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetDeclaration( - GetDeclarations { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer_handle.read(cx)), )), }); let buffer = buffer_handle.clone(); @@ -5368,23 +5473,27 @@ impl LspStore { pub fn type_definitions( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetTypeDefinitions { position }; + if !self.is_capable_for_proto_request(&buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetTypeDefinition( - GetTypeDefinitions { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); - let buffer = buffer_handle.clone(); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { return Ok(Vec::new()); @@ -5423,7 +5532,7 @@ impl LspStore { }) } else { let type_definitions_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetTypeDefinitions { position }, cx, @@ -5441,23 +5550,27 @@ impl LspStore { pub fn implementations( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetImplementations { position }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetImplementation( - GetImplementations { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); - let buffer = buffer_handle.clone(); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { return Ok(Vec::new()); @@ -5496,7 +5609,7 @@ impl LspStore { }) } else { let implementations_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetImplementations { position }, cx, @@ -5514,23 +5627,27 @@ impl LspStore { pub fn references( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetReferences { position }; + if !self.is_capable_for_proto_request(&buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetReferences( - GetReferences { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); - let buffer = buffer_handle.clone(); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { return Ok(Vec::new()); @@ -5569,7 +5686,7 @@ impl LspStore { }) } else { let references_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetReferences { position }, cx, @@ -5587,28 +5704,31 @@ impl LspStore { pub fn code_actions( &mut self, - buffer_handle: &Entity, + buffer: &Entity, range: Range, kinds: Option>, cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetCodeActions { + range: range.clone(), + kinds: kinds.clone(), + }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetCodeActions( - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - } - .to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); - let buffer = buffer_handle.clone(); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { return Ok(Vec::new()); @@ -5650,7 +5770,7 @@ impl LspStore { }) } else { let all_actions_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(range.start), GetCodeActions { range: range.clone(), @@ -5752,6 +5872,10 @@ impl LspStore { cx: &mut Context, ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetCodeLens; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(HashMap::default())); + } let request_task = upstream_client.request(proto::MultiLspQuery { buffer_id: buffer.read(cx).remote_id().into(), version: serialize_version(&buffer.read(cx).version()), @@ -5760,7 +5884,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetCodeLens( - GetCodeLens.to_proto(project_id, buffer.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); let buffer = buffer.clone(); @@ -5844,11 +5968,15 @@ impl LspStore { let language_registry = self.languages.clone(); if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetCompletions { position, context }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let task = self.send_lsp_proto_request( buffer.clone(), upstream_client, project_id, - GetCompletions { position, context }, + request, cx, ); let language = buffer.read(cx).language().cloned(); @@ -5986,11 +6114,17 @@ impl LspStore { cx: &mut Context, ) -> Task> { let client = self.upstream_client(); - let buffer_id = buffer.read(cx).remote_id(); let buffer_snapshot = buffer.read(cx).snapshot(); - cx.spawn(async move |this, cx| { + if !self.check_if_capable_for_proto_request( + &buffer, + GetCompletions::can_resolve_completions, + cx, + ) { + return Task::ready(Ok(false)); + } + cx.spawn(async move |lsp_store, cx| { let mut did_resolve = false; if let Some((client, project_id)) = client { for completion_index in completion_indices { @@ -6027,7 +6161,7 @@ impl LspStore { completion.source.server_id() }; if let Some(server_id) = server_id { - let server_and_adapter = this + let server_and_adapter = lsp_store .read_with(cx, |lsp_store, _| { let server = lsp_store.language_server_for_id(server_id)?; let adapter = @@ -6078,13 +6212,7 @@ impl LspStore { completion_index: usize, ) -> Result<()> { let server_id = server.server_id(); - let can_resolve = server - .capabilities() - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false); - if !can_resolve { + if !GetCompletions::can_resolve_completions(&server.capabilities()) { return Ok(()); } @@ -6435,16 +6563,24 @@ impl LspStore { pub fn pull_diagnostics( &mut self, - buffer_handle: Entity, + buffer: Entity, cx: &mut Context, ) -> Task>> { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); + let buffer_id = buffer.read(cx).remote_id(); if let Some((client, upstream_project_id)) = self.upstream_client() { + if !self.is_capable_for_proto_request( + &buffer, + &GetDocumentDiagnostics { + previous_result_id: None, + }, + cx, + ) { + return Task::ready(Ok(Vec::new())); + } let request_task = client.request(proto::MultiLspQuery { buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer_handle.read(cx).version()), + version: serialize_version(&buffer.read(cx).version()), project_id: upstream_project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, @@ -6453,7 +6589,7 @@ impl LspStore { proto::GetDocumentDiagnostics { project_id: upstream_project_id, buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer_handle.read(cx).version()), + version: serialize_version(&buffer.read(cx).version()), }, )), }); @@ -6475,7 +6611,7 @@ impl LspStore { .collect()) }) } else { - let server_ids = buffer_handle.update(cx, |buffer, cx| { + let server_ids = buffer.update(cx, |buffer, cx| { self.language_servers_for_local_buffer(buffer, cx) .map(|(_, server)| server.server_id()) .collect::>() @@ -6485,7 +6621,7 @@ impl LspStore { .map(|server_id| { let result_id = self.result_id(server_id, buffer_id, cx); self.request_lsp( - buffer_handle.clone(), + buffer.clone(), LanguageServerToQuery::Other(server_id), GetDocumentDiagnostics { previous_result_id: result_id, @@ -6507,34 +6643,36 @@ impl LspStore { pub fn inlay_hints( &mut self, - buffer_handle: Entity, + buffer: Entity, range: Range, cx: &mut Context, ) -> Task>> { - let buffer = buffer_handle.read(cx); let range_start = range.start; let range_end = range.end; - let buffer_id = buffer.remote_id().into(); - let lsp_request = InlayHints { range }; + let buffer_id = buffer.read(cx).remote_id().into(); + let request = InlayHints { range }; if let Some((client, project_id)) = self.upstream_client() { - let request = proto::InlayHints { + if !self.is_capable_for_proto_request(&buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } + let proto_request = proto::InlayHints { project_id, buffer_id, start: Some(serialize_anchor(&range_start)), end: Some(serialize_anchor(&range_end)), - version: serialize_version(&buffer_handle.read(cx).version()), + version: serialize_version(&buffer.read(cx).version()), }; cx.spawn(async move |project, cx| { let response = client - .request(request) + .request(proto_request) .await .context("inlay hints proto request")?; LspCommand::response_from_proto( - lsp_request, + request, response, project.upgrade().context("No project")?, - buffer_handle.clone(), + buffer.clone(), cx.clone(), ) .await @@ -6542,13 +6680,13 @@ impl LspStore { }) } else { let lsp_request_task = self.request_lsp( - buffer_handle.clone(), + buffer.clone(), LanguageServerToQuery::FirstCapable, - lsp_request, + request, cx, ); cx.spawn(async move |_, cx| { - buffer_handle + buffer .update(cx, |buffer, _| { buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp]) })? @@ -6772,6 +6910,11 @@ impl LspStore { cx: &mut Context, ) -> Task>>> { if let Some((client, project_id)) = self.upstream_client() { + let request = GetDocumentColor {}; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(HashMap::default())); + } + let request_task = client.request(proto::MultiLspQuery { project_id, buffer_id: buffer.read(cx).remote_id().to_proto(), @@ -6780,7 +6923,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetDocumentColor( - GetDocumentColor {}.to_proto(project_id, buffer.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); let buffer = buffer.clone(); @@ -6808,7 +6951,7 @@ impl LspStore { } }) .map(|(server_id, color_response)| { - let response = GetDocumentColor {}.response_from_proto( + let response = request.response_from_proto( color_response, project.clone(), buffer.clone(), @@ -6855,6 +6998,10 @@ impl LspStore { let position = position.to_point_utf16(buffer.read(cx)); if let Some((client, upstream_project_id)) = self.upstream_client() { + let request = GetSignatureHelp { position }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Vec::new()); + } let request_task = client.request(proto::MultiLspQuery { buffer_id: buffer.read(cx).remote_id().into(), version: serialize_version(&buffer.read(cx).version()), @@ -6863,7 +7010,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetSignatureHelp( - GetSignatureHelp { position }.to_proto(upstream_project_id, buffer.read(cx)), + request.to_proto(upstream_project_id, buffer.read(cx)), )), }); let buffer = buffer.clone(); @@ -6926,6 +7073,10 @@ impl LspStore { cx: &mut Context, ) -> Task> { if let Some((client, upstream_project_id)) = self.upstream_client() { + let request = GetHover { position }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Vec::new()); + } let request_task = client.request(proto::MultiLspQuery { buffer_id: buffer.read(cx).remote_id().into(), version: serialize_version(&buffer.read(cx).version()), @@ -6934,7 +7085,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetHover( - GetHover { position }.to_proto(upstream_project_id, buffer.read(cx)), + request.to_proto(upstream_project_id, buffer.read(cx)), )), }); let buffer = buffer.clone(); @@ -7539,16 +7690,20 @@ impl LspStore { self.downstream_client = Some((downstream_client.clone(), project_id)); for (server_id, status) in &self.language_server_statuses { - downstream_client - .send(proto::StartLanguageServer { - project_id, - server: Some(proto::LanguageServer { - id: server_id.0 as u64, - name: status.name.clone(), - worktree_id: None, - }), - }) - .log_err(); + if let Some(server) = self.language_server_for_id(*server_id) { + downstream_client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id.to_proto(), + name: status.name.to_string(), + worktree_id: None, + }), + capabilities: serde_json::to_string(&server.capabilities()) + .expect("serializing server LSP capabilities"), + }) + .log_err(); + } } } @@ -7575,7 +7730,7 @@ impl LspStore { ( LanguageServerId(server.id as usize), LanguageServerStatus { - name: server.name, + name: LanguageServerName::from_proto(server.name), pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), @@ -8724,18 +8879,29 @@ impl LspStore { } async fn handle_start_language_server( - this: Entity, + lsp_store: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { let server = envelope.payload.server.context("invalid server")?; - - this.update(&mut cx, |this, cx| { + let server_capabilities = + serde_json::from_str::(&envelope.payload.capabilities) + .with_context(|| { + format!( + "incorrect server capabilities {}", + envelope.payload.capabilities + ) + })?; + lsp_store.update(&mut cx, |lsp_store, cx| { let server_id = LanguageServerId(server.id as usize); - this.language_server_statuses.insert( + let server_name = LanguageServerName::from_proto(server.name.clone()); + lsp_store + .lsp_server_capabilities + .insert(server_id, server_capabilities); + lsp_store.language_server_statuses.insert( server_id, LanguageServerStatus { - name: server.name.clone(), + name: server_name.clone(), pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), @@ -8743,7 +8909,7 @@ impl LspStore { ); cx.emit(LspStoreEvent::LanguageServerAdded( server_id, - LanguageServerName(server.name.into()), + server_name, server.worktree_id.map(WorktreeId::from_proto), )); cx.notify(); @@ -8804,7 +8970,8 @@ impl LspStore { } non_lsp @ proto::update_language_server::Variant::StatusUpdate(_) - | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) => { + | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) + | non_lsp @ proto::update_language_server::Variant::MetadataUpdated(_) => { cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id, name: envelope @@ -10251,7 +10418,7 @@ impl LspStore { let name = self .language_server_statuses .remove(&server_id) - .map(|status| LanguageServerName::from(status.name.as_str())) + .map(|status| status.name.clone()) .or_else(|| { if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() { Some(adapter.name()) @@ -10744,7 +10911,7 @@ impl LspStore { self.language_server_statuses.insert( server_id, LanguageServerStatus { - name: language_server.name().to_string(), + name: language_server.name(), pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), @@ -10758,18 +10925,23 @@ impl LspStore { )); cx.emit(LspStoreEvent::RefreshInlayHints); + let server_capabilities = language_server.capabilities(); if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { downstream_client .send(proto::StartLanguageServer { project_id: *project_id, server: Some(proto::LanguageServer { - id: server_id.0 as u64, + id: server_id.to_proto(), name: language_server.name().to_string(), worktree_id: Some(key.0.to_proto()), }), + capabilities: serde_json::to_string(&server_capabilities) + .expect("serializing server LSP capabilities"), }) .log_err(); } + self.lsp_server_capabilities + .insert(server_id, server_capabilities); // Tell the language server about every open buffer in the worktree that matches the language. // Also check for buffers in worktrees that reused this server @@ -10817,10 +10989,11 @@ impl LspStore { let local = self.as_local_mut().unwrap(); - if local.registered_buffers.contains_key(&buffer.remote_id()) { + let buffer_id = buffer.remote_id(); + if local.registered_buffers.contains_key(&buffer_id) { let versions = local .buffer_snapshots - .entry(buffer.remote_id()) + .entry(buffer_id) .or_default() .entry(server_id) .and_modify(|_| { @@ -10846,10 +11019,10 @@ impl LspStore { version, initial_snapshot.text(), ); - buffer_paths_registered.push(file.abs_path(cx)); + buffer_paths_registered.push((buffer_id, file.abs_path(cx))); local .buffers_opened_in_servers - .entry(buffer.remote_id()) + .entry(buffer_id) .or_default() .insert(server_id); } @@ -10873,13 +11046,14 @@ impl LspStore { } }); - for abs_path in buffer_paths_registered { + for (buffer_id, abs_path) in buffer_paths_registered { cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id: server_id, name: Some(adapter.name()), message: proto::update_language_server::Variant::RegisteredForBuffer( proto::RegisteredForBuffer { buffer_abs_path: abs_path.to_string_lossy().to_string(), + buffer_id: buffer_id.to_proto(), }, ), }); @@ -11337,6 +11511,7 @@ impl LspStore { } fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) { + self.lsp_server_capabilities.remove(&for_server); for buffer_colors in self.lsp_document_colors.values_mut() { buffer_colors.colors.remove(&for_server); buffer_colors.cache_version += 1; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5000ba93be..398e8bde87 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -277,6 +277,13 @@ pub enum Event { LanguageServerAdded(LanguageServerId, LanguageServerName, Option), LanguageServerRemoved(LanguageServerId), LanguageServerLog(LanguageServerId, LanguageServerLogType, String), + // [`lsp::notification::DidOpenTextDocument`] was sent to this server using the buffer data. + // Zed's buffer-related data is updated accordingly. + LanguageServerBufferRegistered { + server_id: LanguageServerId, + buffer_id: BufferId, + buffer_abs_path: PathBuf, + }, Toast { notification_id: SharedString, message: String, @@ -2931,8 +2938,8 @@ impl Project { } LspStoreEvent::LanguageServerUpdate { language_server_id, - message, name, + message, } => { if self.is_local() { self.enqueue_buffer_ordered_message( @@ -2944,6 +2951,32 @@ impl Project { ) .ok(); } + + match message { + proto::update_language_server::Variant::MetadataUpdated(update) => { + if let Some(capabilities) = update + .capabilities + .as_ref() + .and_then(|capabilities| serde_json::from_str(capabilities).ok()) + { + self.lsp_store.update(cx, |lsp_store, _| { + lsp_store + .lsp_server_capabilities + .insert(*language_server_id, capabilities); + }); + } + } + proto::update_language_server::Variant::RegisteredForBuffer(update) => { + if let Some(buffer_id) = BufferId::new(update.buffer_id).ok() { + cx.emit(Event::LanguageServerBufferRegistered { + buffer_id, + server_id: *language_server_id, + buffer_abs_path: PathBuf::from(&update.buffer_abs_path), + }); + } + } + _ => (), + } } LspStoreEvent::Notification(message) => cx.emit(Event::Toast { notification_id: "lsp".into(), @@ -3476,20 +3509,6 @@ impl Project { }) } - fn document_highlights_impl( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task>> { - self.request_lsp( - buffer.clone(), - LanguageServerToQuery::FirstCapable, - GetDocumentHighlights { position }, - cx, - ) - } - pub fn document_highlights( &mut self, buffer: &Entity, @@ -3497,7 +3516,12 @@ impl Project { cx: &mut Context, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.document_highlights_impl(buffer, position, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::FirstCapable, + GetDocumentHighlights { position }, + cx, + ) } pub fn document_symbols( @@ -3598,14 +3622,14 @@ impl Project { .update(cx, |lsp_store, cx| lsp_store.hover(buffer, position, cx)) } - pub fn linked_edit( + pub fn linked_edits( &self, buffer: &Entity, position: Anchor, cx: &mut Context, ) -> Task>>> { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.linked_edit(buffer, position, cx) + lsp_store.linked_edits(buffer, position, cx) }) } @@ -3697,19 +3721,6 @@ impl Project { }) } - fn prepare_rename_impl( - &mut self, - buffer: Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task> { - self.request_lsp( - buffer, - LanguageServerToQuery::FirstCapable, - PrepareRename { position }, - cx, - ) - } pub fn prepare_rename( &mut self, buffer: Entity, @@ -3717,7 +3728,12 @@ impl Project { cx: &mut Context, ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); - self.prepare_rename_impl(buffer, position, cx) + self.request_lsp( + buffer, + LanguageServerToQuery::FirstCapable, + PrepareRename { position }, + cx, + ) } pub fn perform_rename( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 779cf95add..75ebc8339a 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1100,7 +1100,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon let fake_server = fake_servers.next().await.unwrap(); let (server_id, server_name) = lsp_store.read_with(cx, |lsp_store, _| { let (id, status) = lsp_store.language_server_statuses().next().unwrap(); - (id, LanguageServerName::from(status.name.as_str())) + (id, status.name.clone()) }); // Simulate jumping to a definition in a dependency outside of the worktree. @@ -1698,7 +1698,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC name: "the-language-server", disk_based_diagnostics_sources: vec!["disk".into()], disk_based_diagnostics_progress_token: Some(progress_token.into()), - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -1710,6 +1710,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC }) .await .unwrap(); + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); // Simulate diagnostics starting to update. let fake_server = fake_servers.next().await.unwrap(); fake_server.start_progress(progress_token).await; @@ -1736,6 +1737,14 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC ); assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints); fake_server.start_progress(progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::LanguageServerBufferRegistered { + server_id: LanguageServerId(1), + buffer_id, + buffer_abs_path: PathBuf::from(path!("/dir/a.rs")), + } + ); assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsStarted { diff --git a/crates/proto/proto/call.proto b/crates/proto/proto/call.proto index 5212f3b43f..b5c882db56 100644 --- a/crates/proto/proto/call.proto +++ b/crates/proto/proto/call.proto @@ -71,6 +71,7 @@ message RejoinedProject { repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; + repeated string language_server_capabilities = 5; } message LeaveRoom {} @@ -199,6 +200,7 @@ message JoinProjectResponse { repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; + repeated string language_server_capabilities = 8; ChannelRole role = 6; reserved 7; } diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index e3c2f69c0b..1e693dfdf3 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -518,6 +518,7 @@ message LanguageServer { message StartLanguageServer { uint64 project_id = 1; LanguageServer server = 2; + string capabilities = 3; } message UpdateDiagnosticSummary { @@ -545,6 +546,7 @@ message UpdateLanguageServer { LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7; StatusUpdate status_update = 9; RegisteredForBuffer registered_for_buffer = 10; + ServerMetadataUpdated metadata_updated = 11; } } @@ -597,6 +599,11 @@ enum ServerBinaryStatus { message RegisteredForBuffer { string buffer_abs_path = 1; + uint64 buffer_id = 2; +} + +message ServerMetadataUpdated { + optional string capabilities = 1; } message LanguageServerLog { From 064c5daa994989f720afec7a305ffd4539e7c88c Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 5 Aug 2025 10:35:54 -0400 Subject: [PATCH 38/46] docs: Fix incorrect reference to JSX language (#35639) Closes: https://github.com/zed-industries/zed/issues/35633 Release Notes: - N/A --- docs/src/extensions/languages.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index 44c673e3e1..6756cb8a23 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -402,11 +402,10 @@ If your language server supports additional languages, you can use `language_ids [language-servers.my-language-server] name = "Whatever LSP" -languages = ["JavaScript", "JSX", "HTML", "CSS"] +languages = ["JavaScript", "HTML", "CSS"] [language-servers.my-language-server.language_ids] "JavaScript" = "javascript" -"JSX" = "javascriptreact" "TSX" = "typescriptreact" "HTML" = "html" "CSS" = "css" From 351e8c4cd9bb6abba544d90c87c3fed2b6264ca1 Mon Sep 17 00:00:00 2001 From: localcc Date: Tue, 5 Aug 2025 16:36:08 +0200 Subject: [PATCH 39/46] Fix LiveKit audio for devices with different sample formats (#35604) Release Notes: - N/A --- .../src/livekit_client/playback.rs | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index c62b8853b4..f14e156125 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -1,6 +1,7 @@ use anyhow::{Context as _, Result}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _}; +use cpal::{Data, FromSample, I24, SampleFormat, SizedSample}; use futures::channel::mpsc::UnboundedSender; use futures::{Stream, StreamExt as _}; use gpui::{ @@ -258,9 +259,15 @@ impl AudioStack { let stream = device .build_input_stream_raw( &config.config(), - cpal::SampleFormat::I16, + config.sample_format(), move |data, _: &_| { - let mut data = data.as_slice::().unwrap(); + let data = + Self::get_sample_data(config.sample_format(), data).log_err(); + let Some(data) = data else { + return; + }; + let mut data = data.as_slice(); + while data.len() > 0 { let remainder = (buf.capacity() - buf.len()).min(data.len()); buf.extend_from_slice(&data[..remainder]); @@ -313,6 +320,33 @@ impl AudioStack { drop(end_on_drop_tx) } } + + fn get_sample_data(sample_format: SampleFormat, data: &Data) -> Result> { + match sample_format { + SampleFormat::I8 => Ok(Self::convert_sample_data::(data)), + SampleFormat::I16 => Ok(data.as_slice::().unwrap().to_vec()), + SampleFormat::I24 => Ok(Self::convert_sample_data::(data)), + SampleFormat::I32 => Ok(Self::convert_sample_data::(data)), + SampleFormat::I64 => Ok(Self::convert_sample_data::(data)), + SampleFormat::U8 => Ok(Self::convert_sample_data::(data)), + SampleFormat::U16 => Ok(Self::convert_sample_data::(data)), + SampleFormat::U32 => Ok(Self::convert_sample_data::(data)), + SampleFormat::U64 => Ok(Self::convert_sample_data::(data)), + SampleFormat::F32 => Ok(Self::convert_sample_data::(data)), + SampleFormat::F64 => Ok(Self::convert_sample_data::(data)), + _ => anyhow::bail!("Unsupported sample format"), + } + } + + fn convert_sample_data>( + data: &Data, + ) -> Vec { + data.as_slice::() + .unwrap() + .iter() + .map(|e| e.to_sample::()) + .collect() + } } use super::LocalVideoTrack; From 5940ed979fc593190e4b8fbb9b48660d61527874 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:38:08 -0300 Subject: [PATCH 40/46] onboarding: Use a picker for the font dropdowns (#35638) Release Notes: - N/A --- Cargo.lock | 2 + crates/onboarding/Cargo.toml | 2 + crates/onboarding/src/editing_page.rs | 312 ++++++++++++++++---- crates/picker/src/popover_menu.rs | 1 + crates/ui/src/components/dropdown_menu.rs | 24 +- crates/ui/src/components/numeric_stepper.rs | 7 +- 6 files changed, 272 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8e3e81434..4cf5a68f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11039,12 +11039,14 @@ dependencies = [ "editor", "feature_flags", "fs", + "fuzzy", "gpui", "itertools 0.14.0", "language", "language_model", "menu", "notifications", + "picker", "project", "schemars", "serde", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index b3056ff39e..7e76b150a9 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -25,12 +25,14 @@ db.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true +fuzzy.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true language_model.workspace = true menu.workspace = true notifications.workspace = true +picker.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index a5e3a6bf05..6dd272745a 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -2,14 +2,19 @@ use std::sync::Arc; use editor::{EditorSettings, ShowMinimap}; use fs::Fs; -use gpui::{Action, App, FontFeatures, IntoElement, Pixels, Window}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + Action, AnyElement, App, Context, FontFeatures, IntoElement, Pixels, SharedString, Task, Window, +}; use language::language_settings::{AllLanguageSettings, FormatOnSave}; +use picker::{Picker, PickerDelegate}; use project::project_settings::ProjectSettings; use settings::{Settings as _, update_settings_file}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ - ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, - ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, + ButtonLike, ListItem, ListItemSpacing, NumericStepper, PopoverMenu, SwitchField, + ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, + prelude::*, }; use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState}; @@ -246,9 +251,25 @@ fn render_import_settings_section(cx: &App) -> impl IntoElement { fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { let theme_settings = ThemeSettings::get_global(cx); let ui_font_size = theme_settings.ui_font_size(cx); - let font_family = theme_settings.buffer_font.family.clone(); + let ui_font_family = theme_settings.ui_font.family.clone(); + let buffer_font_family = theme_settings.buffer_font.family.clone(); let buffer_font_size = theme_settings.buffer_font_size(cx); + let ui_font_picker = + cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx)); + + let buffer_font_picker = cx.new(|cx| { + font_picker( + buffer_font_family.clone(), + write_buffer_font_family, + window, + cx, + ) + }); + + let ui_font_handle = ui::PopoverMenuHandle::default(); + let buffer_font_handle = ui::PopoverMenuHandle::default(); + h_flex() .w_full() .gap_4() @@ -263,34 +284,35 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl .justify_between() .gap_2() .child( - DropdownMenu::new( - "ui-font-family", - theme_settings.ui_font.family.clone(), - ContextMenu::build(window, cx, |mut menu, _, cx| { - let font_family_cache = FontFamilyCache::global(cx); - - for font_name in font_family_cache.list_font_families(cx) { - menu = menu.custom_entry( - { - let font_name = font_name.clone(); - move |_window, _cx| { - Label::new(font_name.clone()).into_any_element() - } - }, - { - let font_name = font_name.clone(); - move |_window, cx| { - write_ui_font_family(font_name.clone(), cx); - } - }, - ) - } - - menu - }), - ) - .style(ui::DropdownStyle::Outlined) - .full_width(true), + PopoverMenu::new("ui-font-picker") + .menu({ + let ui_font_picker = ui_font_picker.clone(); + move |_window, _cx| Some(ui_font_picker.clone()) + }) + .trigger( + ButtonLike::new("ui-font-family-button") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .full_width() + .child( + h_flex() + .w_full() + .justify_between() + .child(Label::new(ui_font_family)) + .child( + Icon::new(IconName::ChevronUpDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ), + ) + .full_width(true) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(4.0), + }) + .with_handle(ui_font_handle), ) .child( NumericStepper::new( @@ -318,34 +340,35 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl .justify_between() .gap_2() .child( - DropdownMenu::new( - "buffer-font-family", - font_family, - ContextMenu::build(window, cx, |mut menu, _, cx| { - let font_family_cache = FontFamilyCache::global(cx); - - for font_name in font_family_cache.list_font_families(cx) { - menu = menu.custom_entry( - { - let font_name = font_name.clone(); - move |_window, _cx| { - Label::new(font_name.clone()).into_any_element() - } - }, - { - let font_name = font_name.clone(); - move |_window, cx| { - write_buffer_font_family(font_name.clone(), cx); - } - }, - ) - } - - menu - }), - ) - .style(ui::DropdownStyle::Outlined) - .full_width(true), + PopoverMenu::new("buffer-font-picker") + .menu({ + let buffer_font_picker = buffer_font_picker.clone(); + move |_window, _cx| Some(buffer_font_picker.clone()) + }) + .trigger( + ButtonLike::new("buffer-font-family-button") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .full_width() + .child( + h_flex() + .w_full() + .justify_between() + .child(Label::new(buffer_font_family)) + .child( + Icon::new(IconName::ChevronUpDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ), + ) + .full_width(true) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(4.0), + }) + .with_handle(buffer_font_handle), ) .child( NumericStepper::new( @@ -364,6 +387,175 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl ) } +type FontPicker = Picker; + +pub struct FontPickerDelegate { + fonts: Vec, + filtered_fonts: Vec, + selected_index: usize, + current_font: SharedString, + on_font_changed: Arc, +} + +impl FontPickerDelegate { + fn new( + current_font: SharedString, + on_font_changed: impl Fn(SharedString, &mut App) + 'static, + cx: &mut Context, + ) -> Self { + let font_family_cache = FontFamilyCache::global(cx); + + let fonts: Vec = font_family_cache + .list_font_families(cx) + .into_iter() + .collect(); + + let selected_index = fonts + .iter() + .position(|font| *font == current_font) + .unwrap_or(0); + + Self { + fonts: fonts.clone(), + filtered_fonts: fonts + .iter() + .enumerate() + .map(|(index, font)| StringMatch { + candidate_id: index, + string: font.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect(), + selected_index, + current_font, + on_font_changed: Arc::new(on_font_changed), + } + } +} + +impl PickerDelegate for FontPickerDelegate { + type ListItem = AnyElement; + + fn match_count(&self) -> usize { + self.filtered_fonts.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { + self.selected_index = ix.min(self.filtered_fonts.len().saturating_sub(1)); + cx.notify(); + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Search fonts…".into() + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let fonts = self.fonts.clone(); + let current_font = self.current_font.clone(); + + let matches: Vec = if query.is_empty() { + fonts + .iter() + .enumerate() + .map(|(index, font)| StringMatch { + candidate_id: index, + string: font.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + let _candidates: Vec = fonts + .iter() + .enumerate() + .map(|(id, font)| StringMatchCandidate::new(id, font.as_ref())) + .collect(); + + fonts + .iter() + .enumerate() + .filter(|(_, font)| font.to_lowercase().contains(&query.to_lowercase())) + .map(|(index, font)| StringMatch { + candidate_id: index, + string: font.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect() + }; + + let selected_index = if query.is_empty() { + fonts + .iter() + .position(|font| *font == current_font) + .unwrap_or(0) + } else { + matches + .iter() + .position(|m| fonts[m.candidate_id] == current_font) + .unwrap_or(0) + }; + + self.filtered_fonts = matches; + self.selected_index = selected_index; + cx.notify(); + + Task::ready(()) + } + + fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context) { + if let Some(font_match) = self.filtered_fonts.get(self.selected_index) { + let font = font_match.string.clone(); + (self.on_font_changed)(font.into(), cx); + } + } + + fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context) {} + + fn render_match( + &self, + ix: usize, + selected: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let font_match = self.filtered_fonts.get(ix)?; + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(Label::new(font_match.string.clone())) + .into_any_element(), + ) + } +} + +fn font_picker( + current_font: SharedString, + on_font_changed: impl Fn(SharedString, &mut App) + 'static, + window: &mut Window, + cx: &mut Context, +) -> FontPicker { + let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx); + + Picker::list(delegate, window, cx) + .show_scrollbar(true) + .width(rems_from_px(210.)) + .max_height(Some(rems(20.).into())) +} + fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement { const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠."; diff --git a/crates/picker/src/popover_menu.rs b/crates/picker/src/popover_menu.rs index dd1d9c2865..d05308ee71 100644 --- a/crates/picker/src/popover_menu.rs +++ b/crates/picker/src/popover_menu.rs @@ -80,6 +80,7 @@ where { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let picker = self.picker.clone(); + PopoverMenu::new("popover-menu") .menu(move |_window, _cx| Some(picker.clone())) .trigger_with_tooltip(self.trigger, self.tooltip) diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index cdb98086ca..7ad9400f0d 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -276,25 +276,25 @@ impl RenderOnce for DropdownMenuTrigger { .gap_2() .justify_between() .rounded_sm() - .bg(style.bg) - .hover(|s| s.bg(cx.theme().colors().element_hover)) + .map(|this| { + if self.full_width { + this.w_full() + } else { + this.flex_none().w_auto() + } + }) .when(is_outlined, |this| { this.border_1() .border_color(cx.theme().colors().border) .overflow_hidden() }) - .map(|el| { - if self.full_width { - el.w_full() - } else { - el.flex_none().w_auto() - } - }) - .map(|el| { + .map(|this| { if disabled { - el.cursor_not_allowed() + this.cursor_not_allowed() + .bg(cx.theme().colors().element_disabled) } else { - el.cursor_pointer() + this.bg(style.bg) + .hover(|s| s.bg(cx.theme().colors().element_hover)) } }) .child(match self.label { diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs index 5a84633d1b..0ec7111a02 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui/src/components/numeric_stepper.rs @@ -96,7 +96,7 @@ impl RenderOnce for NumericStepper { this.overflow_hidden() .bg(cx.theme().colors().surface_background) .border_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border_variant) } else { this.px_1().bg(cx.theme().colors().editor_background) } @@ -111,7 +111,7 @@ impl RenderOnce for NumericStepper { .justify_center() .hover(|s| s.bg(cx.theme().colors().element_hover)) .border_r_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border_variant) .child(Icon::new(IconName::Dash).size(IconSize::Small)) .on_click(self.on_decrement), ) @@ -124,7 +124,6 @@ impl RenderOnce for NumericStepper { ) } }) - .when(is_outlined, |this| this) .child(Label::new(self.value).mx_3()) .map(|increment| { if is_outlined { @@ -136,7 +135,7 @@ impl RenderOnce for NumericStepper { .justify_center() .hover(|s| s.bg(cx.theme().colors().element_hover)) .border_l_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border_variant) .child(Icon::new(IconName::Plus).size(IconSize::Small)) .on_click(self.on_increment), ) From 19c1504c8ffa460693ee9a0d53292f860255b8d8 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Tue, 5 Aug 2025 19:05:18 +0200 Subject: [PATCH 41/46] ui: Wire up tab indices within buttons (#35368) This change adds the current tab index functionality to buttons and implements a proof of concept for the new welcome page. Primarily blocked on https://github.com/zed-industries/zed/pull/34804, secondarily on https://github.com/zed-industries/zed/pull/35075 so we can ensure navigation always works as intended. Another thing to consider here is whether we want to assign the tab order more implicitly / "automatically" based on the current layout ordering. This would generally enable us to add a default order to focusable elements if we want this. See [the specification](https://html.spec.whatwg.org/multipage/interaction.html#flattened-tabindex-ordered-focus-navigation-scope) on some more context on how the web usually handles this for focusable elements. Release Notes: - N/A --- crates/onboarding/Cargo.toml | 4 ++-- crates/onboarding/src/welcome.rs | 22 +++++++++++++++++-- crates/ui/src/components/button/button.rs | 5 +++++ .../ui/src/components/button/button_like.rs | 18 +++++++++++++-- .../ui/src/components/button/icon_button.rs | 5 +++++ .../ui/src/components/button/toggle_button.rs | 5 +++++ 6 files changed, 53 insertions(+), 6 deletions(-) diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 7e76b150a9..436c714cf3 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -15,13 +15,13 @@ path = "src/onboarding.rs" default = [] [dependencies] -anyhow.workspace = true ai_onboarding.workspace = true +anyhow.workspace = true client.workspace = true command_palette_hooks.workspace = true component.workspace = true -documented.workspace = true db.workspace = true +documented.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 213032f1b3..d4d6c3f701 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -2,6 +2,7 @@ use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, NoAction, ParentElement, Render, Styled, Window, actions, }; +use menu::{SelectNext, SelectPrevious}; use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; use workspace::{ NewFile, Open, WorkspaceId, @@ -124,6 +125,7 @@ impl SectionEntry { cx: &App, ) -> impl IntoElement { ButtonLike::new(("onboarding-button-id", button_index)) + .tab_index(button_index as isize) .full_width() .size(ButtonSize::Medium) .child( @@ -153,10 +155,23 @@ pub struct WelcomePage { focus_handle: FocusHandle, } +impl WelcomePage { + fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { + window.focus_next(); + cx.notify(); + } + + fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { + window.focus_prev(); + cx.notify(); + } +} + impl Render for WelcomePage { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (first_section, second_entries) = CONTENT; + let (first_section, second_section) = CONTENT; let first_section_entries = first_section.entries.len(); + let last_index = first_section_entries + second_section.entries.len(); h_flex() .size_full() @@ -165,6 +180,8 @@ impl Render for WelcomePage { .bg(cx.theme().colors().editor_background) .key_context("Welcome") .track_focus(&self.focus_handle(cx)) + .on_action(cx.listener(Self::select_previous)) + .on_action(cx.listener(Self::select_next)) .child( h_flex() .px_12() @@ -202,7 +219,7 @@ impl Render for WelcomePage { window, cx, )) - .child(second_entries.render( + .child(second_section.render( first_section_entries, &self.focus_handle, window, @@ -220,6 +237,7 @@ impl Render for WelcomePage { .border_dashed() .child( Button::new("welcome-exit", "Return to Setup") + .tab_index(last_index as isize) .full_width() .label_size(LabelSize::XSmall) .on_click(|_, window, cx| { diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index cae5d0e2ca..19f782fb98 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -393,6 +393,11 @@ impl ButtonCommon for Button { self } + fn tab_index(mut self, tab_index: impl Into) -> Self { + self.base = self.base.tab_index(tab_index); + self + } + fn layer(mut self, elevation: ElevationIndex) -> Self { self.base = self.base.layer(elevation); self diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 03f7964f35..15ab00e7e5 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -1,7 +1,7 @@ use documented::Documented; use gpui::{ AnyElement, AnyView, ClickEvent, CursorStyle, DefiniteLength, Hsla, MouseButton, - MouseDownEvent, MouseUpEvent, Rems, relative, transparent_black, + MouseDownEvent, MouseUpEvent, Rems, StyleRefinement, relative, transparent_black, }; use smallvec::SmallVec; @@ -37,6 +37,8 @@ pub trait ButtonCommon: Clickable + Disableable { /// exceptions might a scroll bar, or a slider. fn tooltip(self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self; + fn tab_index(self, tab_index: impl Into) -> Self; + fn layer(self, elevation: ElevationIndex) -> Self; } @@ -393,6 +395,7 @@ pub struct ButtonLike { pub(super) width: Option, pub(super) height: Option, pub(super) layer: Option, + tab_index: Option, size: ButtonSize, rounding: Option, tooltip: Option AnyView>>, @@ -421,6 +424,7 @@ impl ButtonLike { on_click: None, on_right_click: None, layer: None, + tab_index: None, } } @@ -525,6 +529,11 @@ impl ButtonCommon for ButtonLike { self } + fn tab_index(mut self, tab_index: impl Into) -> Self { + self.tab_index = Some(tab_index.into()); + self + } + fn layer(mut self, elevation: ElevationIndex) -> Self { self.layer = Some(elevation); self @@ -554,6 +563,7 @@ impl RenderOnce for ButtonLike { self.base .h_flex() .id(self.id.clone()) + .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) .font_ui(cx) .group("") .flex_none() @@ -591,8 +601,12 @@ impl RenderOnce for ButtonLike { } }) .when(!self.disabled, |this| { + let hovered_style = style.hovered(self.layer, cx); + let focus_color = + |refinement: StyleRefinement| refinement.bg(hovered_style.background); this.cursor(self.cursor_style) - .hover(|hover| hover.bg(style.hovered(self.layer, cx).background)) + .hover(focus_color) + .focus(focus_color) .active(|active| active.bg(style.active(cx).background)) }) .when_some( diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index e5d13e09cd..8d8718a634 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -164,6 +164,11 @@ impl ButtonCommon for IconButton { self } + fn tab_index(mut self, tab_index: impl Into) -> Self { + self.base = self.base.tab_index(tab_index); + self + } + fn layer(mut self, elevation: ElevationIndex) -> Self { self.base = self.base.layer(elevation); self diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index a1e4d65a24..d4d47da9b6 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -121,6 +121,11 @@ impl ButtonCommon for ToggleButton { self } + fn tab_index(mut self, tab_index: impl Into) -> Self { + self.base = self.base.tab_index(tab_index); + self + } + fn layer(mut self, elevation: ElevationIndex) -> Self { self.base = self.base.layer(elevation); self From f017ffdffcdfe7b2947f5045092d84ffe5469f9f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Aug 2025 10:07:30 -0700 Subject: [PATCH 42/46] Fix minidump endpoint configuration (#35646) Release Notes: - N/A --- .github/workflows/ci.yml | 2 +- .github/workflows/nix.yml | 2 +- .github/workflows/release_nightly.yml | 2 +- crates/client/src/telemetry.rs | 6 +++--- crates/zed/src/reliability.rs | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c08f4ac211..f83a3715a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ env: DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} + ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} jobs: job_spec: diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index c019f805fe..6c3a97c163 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -29,7 +29,7 @@ jobs: runs-on: ${{ matrix.system.runner }} env: ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} + ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }} GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on steps: diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 69e5f86cb6..d62aa78293 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -13,7 +13,7 @@ env: CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} + ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 4a8e745fcb..43a1a0b7a4 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -74,10 +74,10 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock>> = LazyLock::new(|| { }) }); -pub static SENTRY_MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { - option_env!("SENTRY_MINIDUMP_ENDPOINT") +pub static MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { + option_env!("ZED_MINIDUMP_ENDPOINT") .map(|s| s.to_owned()) - .or_else(|| env::var("SENTRY_MINIDUMP_ENDPOINT").ok()) + .or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok()) }); static DOTNET_PROJECT_FILES_REGEX: LazyLock = LazyLock::new(|| { diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 9157f66216..ed149a470a 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -4,7 +4,7 @@ use backtrace::{self, Backtrace}; use chrono::Utc; use client::{ TelemetrySettings, - telemetry::{self, SENTRY_MINIDUMP_ENDPOINT}, + telemetry::{self, MINIDUMP_ENDPOINT}, }; use db::kvp::KEY_VALUE_STORE; use futures::AsyncReadExt; @@ -584,7 +584,7 @@ async fn upload_minidump( minidump: Vec, panic: Option<&Panic>, ) -> Result<()> { - let sentry_upload_url = SENTRY_MINIDUMP_ENDPOINT + let minidump_endpoint = MINIDUMP_ENDPOINT .to_owned() .ok_or_else(|| anyhow::anyhow!("Minidump endpoint not set"))?; @@ -605,7 +605,7 @@ async fn upload_minidump( } let mut response_text = String::new(); - let mut response = http.send_multipart_form(&sentry_upload_url, form).await?; + let mut response = http.send_multipart_form(&minidump_endpoint, form).await?; response .body_mut() .read_to_string(&mut response_text) From 844ea3d1ab4ce74577d5a767bbc2ef76974391f9 Mon Sep 17 00:00:00 2001 From: localcc Date: Tue, 5 Aug 2025 19:09:04 +0200 Subject: [PATCH 43/46] Fix open with zed not focusing window (#35645) --- crates/gpui/src/platform/windows/window.rs | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 4043001a35..32a6da2391 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -677,6 +677,36 @@ impl PlatformWindow for WindowsWindow { this.set_window_placement().log_err(); unsafe { SetActiveWindow(hwnd).log_err() }; unsafe { SetFocus(Some(hwnd)).log_err() }; + + // premium ragebait by windows, this is needed because the window + // must have received an input event to be able to set itself to foreground + // so let's just simulate user input as that seems to be the most reliable way + // some more info: https://gist.github.com/Aetopia/1581b40f00cc0cadc93a0e8ccb65dc8c + // bonus: this bug also doesn't manifest if you have vs attached to the process + let inputs = [ + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_MENU, + dwFlags: KEYBD_EVENT_FLAGS(0), + ..Default::default() + }, + }, + }, + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_MENU, + dwFlags: KEYEVENTF_KEYUP, + ..Default::default() + }, + }, + }, + ]; + unsafe { SendInput(&inputs, std::mem::size_of::() as i32) }; + // todo(windows) // crate `windows 0.56` reports true as Err unsafe { SetForegroundWindow(hwnd).as_bool() }; From fc2ba82eb61593b1a2b4d047149b9e591b9f1a4e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:09:42 +0200 Subject: [PATCH 44/46] debugpy: Fetch a wheel into Zed's work dir and use that with users venv (#35640) Another stab at #35388 cc @Sansui233 Closes #35388 Release Notes: - debugger: Fixed Python debug sessions failing to launch due to a missing debugpy installation. --- crates/dap_adapters/src/python.rs | 331 ++++++++++++++++-------------- 1 file changed, 175 insertions(+), 156 deletions(-) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index f499244966..461ce6fbb3 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,38 +1,36 @@ use crate::*; use anyhow::Context as _; use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; +use fs::RemoveOptions; +use futures::{StreamExt, TryStreamExt}; +use gpui::http_client::AsyncBody; use gpui::{AsyncApp, SharedString}; use json_dotpath::DotPaths; use language::LanguageName; use paths::debug_adapters_dir; use serde_json::Value; +use smol::fs::File; +use smol::io::AsyncReadExt; use smol::lock::OnceCell; +use std::ffi::OsString; use std::net::Ipv4Addr; +use std::str::FromStr; use std::{ collections::HashMap, ffi::OsStr, path::{Path, PathBuf}, }; +use util::{ResultExt, maybe}; #[derive(Default)] pub(crate) struct PythonDebugAdapter { - python_venv_base: OnceCell, String>>, + debugpy_whl_base_path: OnceCell, String>>, } impl PythonDebugAdapter { const ADAPTER_NAME: &'static str = "Debugpy"; const DEBUG_ADAPTER_NAME: DebugAdapterName = DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME)); - const PYTHON_ADAPTER_IN_VENV: &'static str = if cfg!(target_os = "windows") { - "Scripts/python3" - } else { - "bin/python3" - }; - const ADAPTER_PATH: &'static str = if cfg!(target_os = "windows") { - "debugpy-venv/Scripts/debugpy-adapter" - } else { - "debugpy-venv/bin/debugpy-adapter" - }; const LANGUAGE_NAME: &'static str = "Python"; @@ -41,7 +39,6 @@ impl PythonDebugAdapter { port: u16, user_installed_path: Option<&Path>, user_args: Option>, - installed_in_venv: bool, ) -> Result> { let mut args = if let Some(user_installed_path) = user_installed_path { log::debug!( @@ -49,13 +46,11 @@ impl PythonDebugAdapter { user_installed_path.display() ); vec![user_installed_path.to_string_lossy().to_string()] - } else if installed_in_venv { - log::debug!("Using venv-installed debugpy"); - vec!["-m".to_string(), "debugpy.adapter".to_string()] } else { let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()); let path = adapter_path - .join(Self::ADAPTER_PATH) + .join("debugpy") + .join("adapter") .to_string_lossy() .into_owned(); log::debug!("Using pip debugpy adapter from: {path}"); @@ -96,63 +91,23 @@ impl PythonDebugAdapter { }) } - async fn ensure_venv(delegate: &dyn DapDelegate) -> Result> { - let python_path = Self::find_base_python(delegate) + async fn fetch_wheel(delegate: &Arc) -> Result, String> { + let system_python = Self::system_python_name(delegate) .await - .context("Could not find Python installation for DebugPy")?; - let work_dir = debug_adapters_dir().join(Self::ADAPTER_NAME); - if !work_dir.exists() { - std::fs::create_dir_all(&work_dir)?; - } - let mut path = work_dir.clone(); - path.push("debugpy-venv"); - if !path.exists() { - util::command::new_smol_command(python_path) - .arg("-m") - .arg("venv") - .arg("debugpy-venv") - .current_dir(work_dir) - .spawn()? - .output() - .await?; - } - - Ok(path.into()) - } - - // Find "baseline", user python version from which we'll create our own venv. - async fn find_base_python(delegate: &dyn DapDelegate) -> Option { - for path in ["python3", "python"] { - if let Some(path) = delegate.which(path.as_ref()).await { - return Some(path); - } - } - None - } - const BINARY_DIR: &str = if cfg!(target_os = "windows") { - "Scripts" - } else { - "bin" - }; - async fn base_venv(&self, delegate: &dyn DapDelegate) -> Result, String> { - self.python_venv_base - .get_or_init(move || async move { - let venv_base = Self::ensure_venv(delegate) - .await - .map_err(|e| format!("{e}"))?; - Self::install_debugpy_into_venv(&venv_base).await?; - Ok(venv_base) - }) - .await - .clone() - } - - async fn install_debugpy_into_venv(venv_path: &Path) -> Result<(), String> { - let pip_path = venv_path.join(Self::BINARY_DIR).join("pip3"); - let installation_succeeded = util::command::new_smol_command(pip_path.as_path()) - .arg("install") - .arg("debugpy") - .arg("-U") + .ok_or_else(|| String::from("Could not find a Python installation"))?; + let command: &OsStr = system_python.as_ref(); + let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels"); + std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?; + let installation_succeeded = util::command::new_smol_command(command) + .args([ + "-m", + "pip", + "download", + "debugpy", + "--only-binary=:all:", + "-d", + download_dir.to_string_lossy().as_ref(), + ]) .output() .await .map_err(|e| format!("{e}"))? @@ -162,7 +117,117 @@ impl PythonDebugAdapter { return Err("debugpy installation failed".into()); } - Ok(()) + let wheel_path = std::fs::read_dir(&download_dir) + .map_err(|e| e.to_string())? + .find_map(|entry| { + entry.ok().filter(|e| { + e.file_type().is_ok_and(|typ| typ.is_file()) + && Path::new(&e.file_name()).extension() == Some("whl".as_ref()) + }) + }) + .ok_or_else(|| String::from("Did not find a .whl in {download_dir}"))?; + + util::archive::extract_zip( + &debug_adapters_dir().join(Self::ADAPTER_NAME), + File::open(&wheel_path.path()) + .await + .map_err(|e| e.to_string())?, + ) + .await + .map_err(|e| e.to_string())?; + + Ok(Arc::from(wheel_path.path())) + } + + async fn maybe_fetch_new_wheel(delegate: &Arc) { + let latest_release = delegate + .http_client() + .get( + "https://pypi.org/pypi/debugpy/json", + AsyncBody::empty(), + false, + ) + .await + .log_err(); + maybe!(async move { + let response = latest_release.filter(|response| response.status().is_success())?; + + let mut output = String::new(); + response + .into_body() + .read_to_string(&mut output) + .await + .ok()?; + let as_json = serde_json::Value::from_str(&output).ok()?; + let latest_version = as_json.get("info").and_then(|info| { + info.get("version") + .and_then(|version| version.as_str()) + .map(ToOwned::to_owned) + })?; + let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into(); + let is_up_to_date = delegate + .fs() + .read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME)) + .await + .ok()? + .into_stream() + .any(async |entry| { + entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname)) + }) + .await; + + if !is_up_to_date { + delegate + .fs() + .remove_dir( + &debug_adapters_dir().join(Self::ADAPTER_NAME), + RemoveOptions { + recursive: true, + ignore_if_not_exists: true, + }, + ) + .await + .ok()?; + Self::fetch_wheel(delegate).await.ok()?; + } + Some(()) + }) + .await; + } + + async fn fetch_debugpy_whl( + &self, + delegate: &Arc, + ) -> Result, String> { + self.debugpy_whl_base_path + .get_or_init(|| async move { + Self::maybe_fetch_new_wheel(delegate).await; + Ok(Arc::from( + debug_adapters_dir() + .join(Self::ADAPTER_NAME) + .join("debugpy") + .join("adapter") + .as_ref(), + )) + }) + .await + .clone() + } + + async fn system_python_name(delegate: &Arc) -> Option { + const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"]; + let mut name = None; + + for cmd in BINARY_NAMES { + name = delegate + .which(OsStr::new(cmd)) + .await + .map(|path| path.to_string_lossy().to_string()); + if name.is_some() { + break; + } + } + name } async fn get_installed_binary( @@ -172,27 +237,14 @@ impl PythonDebugAdapter { user_installed_path: Option, user_args: Option>, python_from_toolchain: Option, - installed_in_venv: bool, ) -> Result { - const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"]; let tcp_connection = config.tcp_connection.clone().unwrap_or_default(); let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; let python_path = if let Some(toolchain) = python_from_toolchain { Some(toolchain) } else { - let mut name = None; - - for cmd in BINARY_NAMES { - name = delegate - .which(OsStr::new(cmd)) - .await - .map(|path| path.to_string_lossy().to_string()); - if name.is_some() { - break; - } - } - name + Self::system_python_name(delegate).await }; let python_command = python_path.context("failed to find binary path for Python")?; @@ -203,7 +255,6 @@ impl PythonDebugAdapter { port, user_installed_path.as_deref(), user_args, - installed_in_venv, ) .await?; @@ -625,14 +676,7 @@ impl DebugAdapter for PythonDebugAdapter { local_path.display() ); return self - .get_installed_binary( - delegate, - &config, - Some(local_path.clone()), - user_args, - None, - false, - ) + .get_installed_binary(delegate, &config, Some(local_path.clone()), user_args, None) .await; } @@ -657,46 +701,28 @@ impl DebugAdapter for PythonDebugAdapter { ) .await; - if let Some(toolchain) = &toolchain { - if let Some(path) = Path::new(&toolchain.path.to_string()).parent() { - if let Some(parent) = path.parent() { - Self::install_debugpy_into_venv(parent).await.ok(); - } - - let debugpy_path = path.join("debugpy"); - if delegate.fs().is_file(&debugpy_path).await { - log::debug!( - "Found debugpy in toolchain environment: {}", - debugpy_path.display() - ); - return self - .get_installed_binary( - delegate, - &config, - None, - user_args, - Some(toolchain.path.to_string()), - true, - ) - .await; - } - } - } - let toolchain = self - .base_venv(&**delegate) + let debugpy_path = self + .fetch_debugpy_whl(delegate) .await - .map_err(|e| anyhow::anyhow!(e))? - .join(Self::PYTHON_ADAPTER_IN_VENV); + .map_err(|e| anyhow::anyhow!("{e}"))?; + if let Some(toolchain) = &toolchain { + log::debug!( + "Found debugpy in toolchain environment: {}", + debugpy_path.display() + ); + return self + .get_installed_binary( + delegate, + &config, + None, + user_args, + Some(toolchain.path.to_string()), + ) + .await; + } - self.get_installed_binary( - delegate, - &config, - None, - user_args, - Some(toolchain.to_string_lossy().into_owned()), - false, - ) - .await + self.get_installed_binary(delegate, &config, None, user_args, None) + .await } fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option { @@ -711,6 +737,8 @@ impl DebugAdapter for PythonDebugAdapter { #[cfg(test)] mod tests { + use util::path; + use super::*; use std::{net::Ipv4Addr, path::PathBuf}; @@ -721,30 +749,24 @@ mod tests { // Case 1: User-defined debugpy path (highest precedence) let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter"); - let user_args = PythonDebugAdapter::generate_debugpy_arguments( - &host, - port, - Some(&user_path), - None, - false, - ) - .await - .unwrap(); - - // Case 2: Venv-installed debugpy (uses -m debugpy.adapter) - let venv_args = - PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None, true) + let user_args = + PythonDebugAdapter::generate_debugpy_arguments(&host, port, Some(&user_path), None) .await .unwrap(); + // Case 2: Venv-installed debugpy (uses -m debugpy.adapter) + let venv_args = PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None) + .await + .unwrap(); + assert_eq!(user_args[0], "/custom/path/to/debugpy/src/debugpy/adapter"); assert_eq!(user_args[1], "--host=127.0.0.1"); assert_eq!(user_args[2], "--port=5678"); - assert_eq!(venv_args[0], "-m"); - assert_eq!(venv_args[1], "debugpy.adapter"); - assert_eq!(venv_args[2], "--host=127.0.0.1"); - assert_eq!(venv_args[3], "--port=5678"); + let expected_suffix = path!("debug_adapters/Debugpy/debugpy/adapter"); + assert!(venv_args[0].ends_with(expected_suffix)); + assert_eq!(venv_args[1], "--host=127.0.0.1"); + assert_eq!(venv_args[2], "--port=5678"); // The same cases, with arguments overridden by the user let user_args = PythonDebugAdapter::generate_debugpy_arguments( @@ -752,7 +774,6 @@ mod tests { port, Some(&user_path), Some(vec!["foo".into()]), - false, ) .await .unwrap(); @@ -761,7 +782,6 @@ mod tests { port, None, Some(vec!["foo".into()]), - true, ) .await .unwrap(); @@ -769,9 +789,8 @@ mod tests { assert!(user_args[0].ends_with("src/debugpy/adapter")); assert_eq!(user_args[1], "foo"); - assert_eq!(venv_args[0], "-m"); - assert_eq!(venv_args[1], "debugpy.adapter"); - assert_eq!(venv_args[2], "foo"); + assert!(venv_args[0].ends_with(expected_suffix)); + assert_eq!(venv_args[1], "foo"); // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API. } From 307d709adb4801347c6d3e4347a1b1ffc3743540 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 5 Aug 2025 14:09:21 -0400 Subject: [PATCH 45/46] ci: Double Buildjet ARM runner size (24GB to 48GB ram) (#35654) Release Notes: - N/A --- .github/actionlint.yml | 1 - .github/workflows/ci.yml | 2 +- .github/workflows/release_nightly.yml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index d93ec5b15e..6bfbc27705 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -24,7 +24,6 @@ self-hosted-runner: - buildjet-8vcpu-ubuntu-2204-arm - buildjet-16vcpu-ubuntu-2204-arm - buildjet-32vcpu-ubuntu-2204-arm - - buildjet-64vcpu-ubuntu-2204-arm # Self Hosted Runners - self-mini-macos - self-32vcpu-windows-2022 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f83a3715a8..43d305faae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -650,7 +650,7 @@ jobs: timeout-minutes: 60 name: Linux arm64 release bundle runs-on: - - buildjet-16vcpu-ubuntu-2204-arm + - buildjet-32vcpu-ubuntu-2204-arm if: | startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index d62aa78293..c847149984 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -168,7 +168,7 @@ jobs: name: Create a Linux *.tar.gz bundle for ARM if: github.repository_owner == 'zed-industries' runs-on: - - buildjet-16vcpu-ubuntu-2204-arm + - buildjet-32vcpu-ubuntu-2204-arm needs: tests steps: - name: Checkout repo From 0b5592d788f6e8dcc3b13016a27295f0a9a85b1a Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 5 Aug 2025 14:16:47 -0400 Subject: [PATCH 46/46] Add Claude Opus 4.1 (#35653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-08-05 at 1 55 35 PM Release Notes: - Added support for Claude Opus 4.1 Co-authored-by: Marshall Bowers --- crates/anthropic/src/anthropic.rs | 30 +++++++++++++++++++++ crates/bedrock/src/models.rs | 44 ++++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index c73f606045..3ff1666755 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -36,11 +36,18 @@ pub enum AnthropicModelMode { pub enum Model { #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")] ClaudeOpus4, + #[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")] + ClaudeOpus4_1, #[serde( rename = "claude-opus-4-thinking", alias = "claude-opus-4-thinking-latest" )] ClaudeOpus4Thinking, + #[serde( + rename = "claude-opus-4-1-thinking", + alias = "claude-opus-4-1-thinking-latest" + )] + ClaudeOpus4_1Thinking, #[default] #[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")] ClaudeSonnet4, @@ -91,10 +98,18 @@ impl Model { } pub fn from_id(id: &str) -> Result { + if id.starts_with("claude-opus-4-1-thinking") { + return Ok(Self::ClaudeOpus4_1Thinking); + } + if id.starts_with("claude-opus-4-thinking") { return Ok(Self::ClaudeOpus4Thinking); } + if id.starts_with("claude-opus-4-1") { + return Ok(Self::ClaudeOpus4_1); + } + if id.starts_with("claude-opus-4") { return Ok(Self::ClaudeOpus4); } @@ -141,7 +156,9 @@ impl Model { pub fn id(&self) -> &str { match self { Self::ClaudeOpus4 => "claude-opus-4-latest", + Self::ClaudeOpus4_1 => "claude-opus-4-1-latest", Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest", + Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest", Self::ClaudeSonnet4 => "claude-sonnet-4-latest", Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest", Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", @@ -159,6 +176,7 @@ impl Model { pub fn request_id(&self) -> &str { match self { Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514", + Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805", Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514", Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest", @@ -173,7 +191,9 @@ impl Model { pub fn display_name(&self) -> &str { match self { Self::ClaudeOpus4 => "Claude Opus 4", + Self::ClaudeOpus4_1 => "Claude Opus 4.1", Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", + Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking", Self::ClaudeSonnet4 => "Claude Sonnet 4", Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", Self::Claude3_7Sonnet => "Claude 3.7 Sonnet", @@ -192,7 +212,9 @@ impl Model { pub fn cache_configuration(&self) -> Option { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -215,7 +237,9 @@ impl Model { pub fn max_token_count(&self) -> u64 { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -232,7 +256,9 @@ impl Model { pub fn max_output_tokens(&self) -> u64 { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -249,7 +275,9 @@ impl Model { pub fn default_temperature(&self) -> f32 { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -269,6 +297,7 @@ impl Model { pub fn mode(&self) -> AnthropicModelMode { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeSonnet4 | Self::Claude3_5Sonnet | Self::Claude3_7Sonnet @@ -277,6 +306,7 @@ impl Model { | Self::Claude3Sonnet | Self::Claude3Haiku => AnthropicModelMode::Default, Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4Thinking | Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking { budget_tokens: Some(4_096), diff --git a/crates/bedrock/src/models.rs b/crates/bedrock/src/models.rs index b6eeafa2d6..69d2ffb845 100644 --- a/crates/bedrock/src/models.rs +++ b/crates/bedrock/src/models.rs @@ -32,11 +32,18 @@ pub enum Model { ClaudeSonnet4Thinking, #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")] ClaudeOpus4, + #[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")] + ClaudeOpus4_1, #[serde( rename = "claude-opus-4-thinking", alias = "claude-opus-4-thinking-latest" )] ClaudeOpus4Thinking, + #[serde( + rename = "claude-opus-4-1-thinking", + alias = "claude-opus-4-1-thinking-latest" + )] + ClaudeOpus4_1Thinking, #[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")] Claude3_5SonnetV2, #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")] @@ -147,7 +154,9 @@ impl Model { Model::ClaudeSonnet4 => "claude-4-sonnet", Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking", Model::ClaudeOpus4 => "claude-4-opus", + Model::ClaudeOpus4_1 => "claude-4-opus-1", Model::ClaudeOpus4Thinking => "claude-4-opus-thinking", + Model::ClaudeOpus4_1Thinking => "claude-4-opus-1-thinking", Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2", Model::Claude3_5Sonnet => "claude-3-5-sonnet", Model::Claude3Opus => "claude-3-opus", @@ -208,6 +217,9 @@ impl Model { Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => { "anthropic.claude-opus-4-20250514-v1:0" } + Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => { + "anthropic.claude-opus-4-1-20250805-v1:0" + } Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0", Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0", Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0", @@ -266,7 +278,9 @@ impl Model { Self::ClaudeSonnet4 => "Claude Sonnet 4", Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", Self::ClaudeOpus4 => "Claude Opus 4", + Self::ClaudeOpus4_1 => "Claude Opus 4.1", Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", + Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking", Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2", Self::Claude3_5Sonnet => "Claude 3.5 Sonnet", Self::Claude3Opus => "Claude 3 Opus", @@ -330,8 +344,10 @@ impl Model { | Self::Claude3_7Sonnet | Self::ClaudeSonnet4 | Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeSonnet4Thinking - | Self::ClaudeOpus4Thinking => 200_000, + | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking => 200_000, Self::AmazonNovaPremier => 1_000_000, Self::PalmyraWriterX5 => 1_000_000, Self::PalmyraWriterX4 => 128_000, @@ -348,7 +364,9 @@ impl Model { | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::ClaudeOpus4 - | Model::ClaudeOpus4Thinking => 128_000, + | Model::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Model::ClaudeOpus4_1Thinking => 128_000, Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192, Self::Custom { max_output_tokens, .. @@ -366,6 +384,8 @@ impl Model { | Self::Claude3_7Sonnet | Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 1.0, Self::Custom { @@ -387,6 +407,8 @@ impl Model { | Self::Claude3_7SonnetThinking | Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Haiku => true, @@ -420,7 +442,9 @@ impl Model { | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::ClaudeOpus4 - | Self::ClaudeOpus4Thinking => true, + | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Self::ClaudeOpus4_1Thinking => true, // Custom models - check if they have cache configuration Self::Custom { @@ -440,7 +464,9 @@ impl Model { | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::ClaudeOpus4 - | Self::ClaudeOpus4Thinking => Some(BedrockModelCacheConfiguration { + | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration { max_cache_anchors: 4, min_total_token: 1024, }), @@ -467,9 +493,11 @@ impl Model { Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking { budget_tokens: Some(4096), }, - Model::ClaudeOpus4Thinking => BedrockModelMode::Thinking { - budget_tokens: Some(4096), - }, + Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => { + BedrockModelMode::Thinking { + budget_tokens: Some(4096), + } + } _ => BedrockModelMode::Default, } } @@ -518,6 +546,8 @@ impl Model { | Model::ClaudeSonnet4Thinking | Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking + | Model::ClaudeOpus4_1 + | Model::ClaudeOpus4_1Thinking | Model::Claude3Haiku | Model::Claude3Opus | Model::Claude3Sonnet